Compare commits
116
Commits
v3.13.0.336
...
v3.14.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5803235407 | ||
|
|
afbf567fef | ||
|
|
72b3c1981a | ||
|
|
72ee82543b | ||
|
|
996c9f4c86 | ||
|
|
f8c1e97376 | ||
|
|
f146040ff3 | ||
|
|
0da28173ab | ||
|
|
be822666a7 | ||
|
|
8c99ee3b0d | ||
|
|
0e3b4a7853 | ||
|
|
615b1dbaae | ||
|
|
0fa51f830d | ||
|
|
007bb345d0 | ||
|
|
46536f11d3 | ||
|
|
b1232b5dc8 | ||
|
|
f8f047d67e | ||
|
|
86529c6f15 | ||
|
|
d63829c344 | ||
|
|
34359a7e86 | ||
|
|
aa335d8365 | ||
|
|
a565d3b7ce | ||
|
|
3bff3c7f23 | ||
|
|
4e6e832e9c | ||
|
|
d6648a840e | ||
|
|
f8c2af716f | ||
|
|
bd812ccb67 | ||
|
|
dad190f7e6 | ||
|
|
1b1ba64a38 | ||
|
|
e31268c39d | ||
|
|
cb9d32dc91 | ||
|
|
36568df5b1 | ||
|
|
b4ac42d789 | ||
|
|
32aafb11a3 | ||
|
|
36dab70b7a | ||
|
|
ebf253b1a7 | ||
|
|
539dcca5ca | ||
|
|
659b6c2a70 | ||
|
|
3dcc7ab311 | ||
|
|
e0ccb58272 | ||
|
|
d4ec8ebec8 | ||
|
|
f3b21e5099 | ||
|
|
202fd38180 | ||
|
|
f5768f2f95 | ||
|
|
2d377d06de | ||
|
|
42d1581ba0 | ||
|
|
995a23c2bc | ||
|
|
8969f55a1f | ||
|
|
312a285792 | ||
|
|
d85b052195 | ||
|
|
cd0d3d1b97 | ||
|
|
6ccc7df998 | ||
|
|
73e5909a69 | ||
|
|
ade487277d | ||
|
|
e755d17977 | ||
|
|
292288de9e | ||
|
|
57f6dec462 | ||
|
|
8e1a549c9d | ||
|
|
1d18eae377 | ||
|
|
6ef04d2fa5 | ||
|
|
1778e9d916 | ||
|
|
dbb63e9faa | ||
|
|
6b1c3182eb | ||
|
|
82460ebbcb | ||
|
|
3e8a9bf1c4 | ||
|
|
a2db925e86 | ||
|
|
fe70cc3daf | ||
|
|
5991112cd8 | ||
|
|
084d91c62a | ||
|
|
4963f7a5f0 | ||
|
|
f42d5219c2 | ||
|
|
101a9b5d5e | ||
|
|
7cd79a4e2a | ||
|
|
30c3acfb23 | ||
|
|
31f04db555 | ||
|
|
7d41f7bdd3 | ||
|
|
8e6ac947ed | ||
|
|
20e321f502 | ||
|
|
00b323e507 | ||
|
|
333b593c8a | ||
|
|
665f6df142 | ||
|
|
9e16130537 | ||
|
|
5c98d6734d | ||
|
|
c3801a3445 | ||
|
|
1d137a3a88 | ||
|
|
2e8b9ae3ca | ||
|
|
fb750e8717 | ||
|
|
f9ae0902d8 | ||
|
|
8d69cbc02f | ||
|
|
90c2372c92 | ||
|
|
84c4943de5 | ||
|
|
057943b6c0 | ||
|
|
e943cb0aec | ||
|
|
70290e205b | ||
|
|
a73b8c7172 | ||
|
|
a5942c8e91 | ||
|
|
dc6a97520b | ||
|
|
8be3f04cc8 | ||
|
|
8d85b1537d | ||
|
|
b16f2a6ce6 | ||
|
|
e19f175e29 | ||
|
|
f3a5b2a99d | ||
|
|
eac2740ea8 | ||
|
|
a06da53ec9 | ||
|
|
765ea24d22 | ||
|
|
bd24ab37f5 | ||
|
|
c689272c42 | ||
|
|
952cc5dd37 | ||
|
|
356df5d630 | ||
|
|
0e433c5f4b | ||
|
|
a28509de85 | ||
|
|
e45601ecda | ||
|
|
5bbf5cbfc8 | ||
|
|
afb98c20ce | ||
|
|
6b2bc4606b | ||
|
|
6b57cec576 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
code_review:
|
||||
pull_request_opened:
|
||||
code_review: false
|
||||
@@ -48,3 +48,9 @@ jobs:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-deb
|
||||
path: HMCL/build/libs/HMCL-*.deb
|
||||
archive: false
|
||||
@@ -46,16 +46,20 @@ jobs:
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.sha256"
|
||||
- name: Verify artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
|
||||
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
|
||||
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
|
||||
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.sha256 | tr -d '\n')
|
||||
|
||||
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
|
||||
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
|
||||
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
|
||||
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
@@ -67,6 +71,7 @@ jobs:
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> RELEASE_NOTE
|
||||
|
||||
# CNB Release Note
|
||||
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
|
||||
@@ -76,6 +81,8 @@ jobs:
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
|
||||
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
|
||||
@@ -86,6 +93,7 @@ jobs:
|
||||
"HMCL-${{ env.HMCL_VERSION }}.exe" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.jar" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.sh" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.deb" \
|
||||
--target "${{ env.HMCL_COMMIT_SHA }}" \
|
||||
--title "${{ env.HMCL_TAG_NAME }}" \
|
||||
--notes-file RELEASE_NOTE \
|
||||
@@ -120,6 +128,9 @@ jobs:
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.sh"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.deb"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
|
||||
env:
|
||||
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
|
||||
CNB_REPO: HMCL-dev/HMCL
|
||||
+7
-1
@@ -64,4 +64,10 @@ language-subtag-registry
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
/.hmcl/
|
||||
/.hmcl/
|
||||
|
||||
# AI
|
||||
/.gemini/
|
||||
!/.gemini/config.yaml
|
||||
.codex
|
||||
/.gradle-*/
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Hello Minecraft! Launcher Copyright (C) &#36;today.year 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/>." />
|
||||
<option name="myName" value="HMCL" />
|
||||
</copyright>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="HMCL">
|
||||
<LanguageOptions name="JAVA">
|
||||
<option name="fileTypeOverride" value="3" />
|
||||
<option name="addBlankAfter" value="false" />
|
||||
</LanguageOptions>
|
||||
</settings>
|
||||
</component>
|
||||
@@ -0,0 +1,27 @@
|
||||
# Java Code Style Requirements
|
||||
|
||||
These rules apply to all Java code written or modified in this repository.
|
||||
|
||||
## Nullability
|
||||
|
||||
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
|
||||
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
|
||||
- Nullability must never be implicit.
|
||||
|
||||
## Immutability
|
||||
|
||||
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
|
||||
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Every class, field, and method must have documentation.
|
||||
- Documentation must use `///` Markdown-style Javadoc comments.
|
||||
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
|
||||
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
|
||||
|
||||
## Gradle
|
||||
|
||||
- When invoking Gradle in this repository, always set `GRADLE_USER_HOME` to the workspace-local `.gradle-user-home` directory.
|
||||
- Prefer commands such as `./gradlew -g .gradle-user-home ...` or the equivalent environment-variable-based configuration.
|
||||
- When running Gradle `test` tasks, use a higher timeout of ten minutes.
|
||||
+40
-10
@@ -6,6 +6,8 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
@@ -56,7 +58,8 @@ dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.jwebp)
|
||||
implementation(libs.fxsvgimage)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
implementation(libs.nayuki.qrcodegen)
|
||||
@@ -194,7 +197,7 @@ tasks.shadowJar {
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||
|
||||
listOf(
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
|
||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||
).forEach { exclude("com/sun/jna/$it/**") }
|
||||
|
||||
@@ -250,20 +253,22 @@ tasks.processResources {
|
||||
}
|
||||
}
|
||||
|
||||
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
inputs.file(jarPath)
|
||||
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
|
||||
outputs.files(extensions.map { artifactFile(it) })
|
||||
|
||||
doLast {
|
||||
val jarContent = jarPath.readBytes()
|
||||
|
||||
ZipFile(jarPath).use { zipFile ->
|
||||
for (extension in extensions) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
|
||||
val output = artifactFile(extension)
|
||||
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
||||
?: throw GradleException("HMCLauncher.$extension not found")
|
||||
|
||||
@@ -278,8 +283,31 @@ val makeExecutables by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
val makeDeb by tasks.registering(CreateDeb::class) {
|
||||
dependsOn(makeExecutables)
|
||||
|
||||
val debFile = layout.file(provider { artifactFile("deb") })
|
||||
|
||||
val debChannel = when (versionType) {
|
||||
"stable" -> ReleaseType.STABLE
|
||||
"dev" -> ReleaseType.DEVELOPMENT
|
||||
else -> ReleaseType.NIGHTLY
|
||||
}
|
||||
|
||||
version.set(project.version.toString())
|
||||
releaseType.set(debChannel)
|
||||
appShFile.set(layout.file(provider { artifactFile("sh") }))
|
||||
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
|
||||
outputFile.set(debFile)
|
||||
|
||||
doLast {
|
||||
createChecksum(debFile.get().asFile)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(makeExecutables)
|
||||
dependsOn(makeDeb)
|
||||
}
|
||||
|
||||
fun parseToolOptions(options: String?): MutableList<String> {
|
||||
@@ -376,12 +404,14 @@ val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgra
|
||||
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
|
||||
val source = layout.projectDirectory.file("terracotta-template.json");
|
||||
|
||||
classifiers.set(listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
))
|
||||
classifiers.set(
|
||||
listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
)
|
||||
)
|
||||
|
||||
version.set(libs.versions.terracotta)
|
||||
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -23,21 +23,13 @@ import com.jfoenix.controls.events.JFXDialogEvent;
|
||||
import com.jfoenix.converters.DialogTransitionConverter;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -379,6 +371,14 @@ public class JFXDialog extends StackPane {
|
||||
setCycleDuration(Duration.seconds(0.4));
|
||||
setDelay(Duration.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void starting() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopping() {
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
|
||||
@@ -231,10 +231,11 @@ public class JFXRippler extends StackPane {
|
||||
*
|
||||
* @return the ripple radius size
|
||||
*/
|
||||
protected double computeRippleRadius() {
|
||||
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
|
||||
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
|
||||
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
|
||||
protected double computeRippleRadius(double centerX, double centerY) {
|
||||
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
|
||||
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
|
||||
|
||||
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
|
||||
}
|
||||
|
||||
protected void setOverLayBounds(Rectangle overlay) {
|
||||
@@ -473,7 +474,7 @@ public class JFXRippler extends StackPane {
|
||||
super(centerX,
|
||||
centerY,
|
||||
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : getRipplerRadius(), null);
|
||||
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
@@ -518,7 +519,7 @@ public class JFXRippler extends StackPane {
|
||||
new KeyValue(opacityProperty(),
|
||||
1,
|
||||
RIPPLE_INTERPOLATOR)
|
||||
), new KeyFrame(Duration.millis(900), inKeyValues));
|
||||
), new KeyFrame(Duration.millis(600), inKeyValues));
|
||||
|
||||
setScaleX(0);
|
||||
setScaleY(0);
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
||||
protected final ObjectProperty<Paint> promptTextFill;
|
||||
private boolean invalid = true;
|
||||
private final StackPane customPane;
|
||||
private final StackPane line = new StackPane();
|
||||
private final StackPane focusedLine = new StackPane();
|
||||
private final Text promptText = new Text();
|
||||
private final double initScale = 0.05;
|
||||
private final Scale scale;
|
||||
private final Timeline linesAnimation;
|
||||
private ParallelTransition transition;
|
||||
private CachedTransition promptTextUpTransition;
|
||||
private CachedTransition promptTextDownTransition;
|
||||
private CachedTransition promptTextColorTransition;
|
||||
private final Scale promptTextScale;
|
||||
private Paint oldPromptTextFill;
|
||||
private final BooleanBinding usePromptText;
|
||||
|
||||
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
|
||||
super(comboBox);
|
||||
this.scale = new Scale(this.initScale, 1.0F);
|
||||
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
|
||||
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
|
||||
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
|
||||
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
|
||||
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
this.promptText.textProperty().bind(comboBox.promptTextProperty());
|
||||
this.promptText.fillProperty().bind(this.promptTextFill);
|
||||
this.promptText.getStyleClass().addAll("text", "prompt-text");
|
||||
this.promptText.getTransforms().add(this.promptTextScale);
|
||||
if (!comboBox.isLabelFloat()) {
|
||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
||||
}
|
||||
|
||||
this.customPane = new StackPane();
|
||||
this.customPane.setMouseTransparent(true);
|
||||
this.customPane.getStyleClass().add("combo-box-button-container");
|
||||
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
|
||||
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
this.customPane.getChildren().add(this.promptText);
|
||||
this.getChildren().add(0, this.customPane);
|
||||
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
|
||||
this.line.getStyleClass().add("input-line");
|
||||
this.focusedLine.getStyleClass().add("input-focused-line");
|
||||
this.getChildren().add(this.line);
|
||||
this.getChildren().add(this.focusedLine);
|
||||
this.line.setPrefHeight(1.0F);
|
||||
this.line.setTranslateY(1.0F);
|
||||
this.line.setManaged(false);
|
||||
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
if (this.getSkinnable().isDisabled()) {
|
||||
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
|
||||
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
this.focusedLine.setPrefHeight(2.0F);
|
||||
this.focusedLine.setTranslateY(0.0F);
|
||||
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
this.focusedLine.setOpacity(0.0F);
|
||||
this.focusedLine.getTransforms().add(this.scale);
|
||||
this.focusedLine.setManaged(false);
|
||||
if (comboBox.isEditable()) {
|
||||
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
|
||||
comboBox.getEditor().promptTextProperty().unbind();
|
||||
comboBox.getEditor().setPromptText(null);
|
||||
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
|
||||
this.usePromptText.invalidate();
|
||||
comboBox.setValue(this.getConverter().fromString(newVal));
|
||||
});
|
||||
}
|
||||
|
||||
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.promptText.visibleProperty().unbind();
|
||||
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
|
||||
} else {
|
||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
||||
}
|
||||
|
||||
this.createFocusTransition();
|
||||
});
|
||||
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal != null) {
|
||||
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(160.0F));
|
||||
}
|
||||
|
||||
protected void starting() {
|
||||
super.starting();
|
||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
||||
}
|
||||
};
|
||||
this.transition = null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal != null) {
|
||||
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
|
||||
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
|
||||
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
});
|
||||
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.focus();
|
||||
} else {
|
||||
this.unFocus();
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
this.customPane.resizeRelocate(x, y, w, h);
|
||||
if (this.invalid) {
|
||||
this.invalid = false;
|
||||
if (!this.getSkinnable().isEditable()) {
|
||||
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
|
||||
if (javaPromptText != null) {
|
||||
this.promptTextFill.set(javaPromptText.getFill());
|
||||
}
|
||||
}
|
||||
|
||||
this.createFloatingAnimation();
|
||||
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
|
||||
this.animateFloatingLabel(true);
|
||||
}
|
||||
}
|
||||
|
||||
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
|
||||
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
|
||||
this.scale.setPivotX(w / (double) 2.0F);
|
||||
}
|
||||
|
||||
private void createFloatingAnimation() {
|
||||
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(240.0F));
|
||||
}
|
||||
};
|
||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(160.0F));
|
||||
}
|
||||
|
||||
protected void starting() {
|
||||
super.starting();
|
||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
||||
}
|
||||
};
|
||||
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(240.0F));
|
||||
}
|
||||
};
|
||||
this.promptTextDownTransition.setOnFinished((finish) -> {
|
||||
this.promptText.setTranslateY(0.0F);
|
||||
this.promptTextScale.setX(1.0F);
|
||||
this.promptTextScale.setY(1.0F);
|
||||
});
|
||||
}
|
||||
|
||||
private void focus() {
|
||||
if (this.transition == null) {
|
||||
this.createFocusTransition();
|
||||
}
|
||||
|
||||
this.transition.play();
|
||||
}
|
||||
|
||||
private void animateFloatingLabel(boolean up) {
|
||||
if (this.promptText == null) {
|
||||
Platform.runLater(() -> this.animateFloatingLabel(up));
|
||||
} else {
|
||||
if (this.transition != null) {
|
||||
this.transition.stop();
|
||||
this.transition.getChildren().remove(this.promptTextUpTransition);
|
||||
this.transition.getChildren().remove(this.promptTextColorTransition);
|
||||
this.transition = null;
|
||||
}
|
||||
|
||||
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
|
||||
this.promptTextDownTransition.stop();
|
||||
this.promptTextUpTransition.play();
|
||||
if (this.getSkinnable().isFocused()) {
|
||||
this.promptTextColorTransition.play();
|
||||
}
|
||||
} else if (!up) {
|
||||
this.promptTextUpTransition.stop();
|
||||
if (this.getSkinnable().isFocused()) {
|
||||
this.promptTextFill.set(this.oldPromptTextFill);
|
||||
}
|
||||
|
||||
this.promptTextDownTransition.play();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void createFocusTransition() {
|
||||
this.transition = new ParallelTransition();
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.transition.getChildren().add(this.promptTextUpTransition);
|
||||
this.transition.getChildren().add(this.promptTextColorTransition);
|
||||
}
|
||||
|
||||
this.transition.getChildren().add(this.linesAnimation);
|
||||
}
|
||||
|
||||
private void unFocus() {
|
||||
if (this.transition != null) {
|
||||
this.transition.stop();
|
||||
}
|
||||
|
||||
this.scale.setX(this.initScale);
|
||||
this.focusedLine.setOpacity(0.0F);
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
|
||||
this.promptTextFill.set(this.oldPromptTextFill);
|
||||
if (this.usePromptText()) {
|
||||
this.promptTextDownTransition.play();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean usePromptText() {
|
||||
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
|
||||
String promptTxt = this.getSkinnable().getPromptText();
|
||||
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,16 @@ import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
*/
|
||||
@@ -78,7 +82,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
|
||||
// create JFX Decorator
|
||||
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
|
||||
pickerDecorator.setOnCloseButtonAction(this::updateColor);
|
||||
pickerDecorator.setOnCloseButtonAction(this::close);
|
||||
pickerDecorator.setPickOnBounds(false);
|
||||
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
|
||||
StyleSheets.init(customScene);
|
||||
@@ -153,6 +157,22 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
paraTransition.play();
|
||||
});
|
||||
|
||||
container.getChildren().add(tabs);
|
||||
|
||||
HBox actionsHBox = new HBox();
|
||||
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(event -> updateColor());
|
||||
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(event -> close());
|
||||
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
|
||||
|
||||
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
|
||||
container.getChildren().add(actionsHBox);
|
||||
|
||||
initRun = () -> {
|
||||
// change tabs labels font color according to the selected color
|
||||
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
|
||||
@@ -232,7 +252,13 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return pane.getBackground().getFills().get(0).getFill();
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
|
||||
return ColorScheme.newBuilder(Themes.getColorScheme())
|
||||
.setPrimaryColorSeed(fill)
|
||||
.build()
|
||||
.getPrimary();
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(() -> {
|
||||
@@ -262,9 +288,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
container.getChildren().add(tabs);
|
||||
|
||||
this.getChildren().add(container);
|
||||
this.setPadding(new Insets(0));
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
@@ -49,6 +50,8 @@ public final class EntryPoint {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkWine();
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
@@ -220,6 +223,20 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkWine() {
|
||||
if (OperatingSystem.isRunningUnderWine()) {
|
||||
SwingUtils.initLookAndFeel();
|
||||
LOG.warning("HMCL is running under Wine or its distributions!");
|
||||
|
||||
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
|
||||
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
|
||||
@@ -17,35 +17,34 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
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;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
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 org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -65,8 +64,8 @@ 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;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||
@@ -111,7 +110,7 @@ public final class Launcher extends Application {
|
||||
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)
|
||||
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
|
||||
return;
|
||||
} else {
|
||||
checkConfigInTempDir();
|
||||
@@ -137,6 +136,9 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
Themes.applyNativeDarkMode(primaryStage);
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -182,6 +184,27 @@ public final class Launcher extends Application {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
|
||||
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
|
||||
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
|
||||
|
||||
okButton.setDisable(true);
|
||||
|
||||
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
|
||||
for (int i = 0; i < seconds; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
||||
}
|
||||
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
|
||||
new KeyValue(okButton.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(okButton.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
alert.setOnShown(e -> timeline.play());
|
||||
alert.setOnCloseRequest(e -> timeline.stop());
|
||||
return alert.showAndWait().orElse(null) == ButtonType.YES;
|
||||
}
|
||||
|
||||
private static boolean isConfigInTempDir() {
|
||||
String configPath = ConfigHolder.configLocation().toString();
|
||||
|
||||
@@ -221,7 +244,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) {
|
||||
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
|
||||
EntryPoint.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -321,6 +344,8 @@ public final class Launcher extends Application {
|
||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||
}
|
||||
|
||||
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
|
||||
|
||||
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
||||
|
||||
launch(Launcher.class, args);
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
@@ -46,9 +45,9 @@ public final class Metadata {
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
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 MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
|
||||
|
||||
public static final String DOCS_URL = "https://docs.hmcl.net";
|
||||
public static final String CONTACT_URL = DOCS_URL + "/help.html";
|
||||
@@ -59,34 +58,38 @@ public final class Metadata {
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
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");
|
||||
if (hmclHome == null) {
|
||||
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
|
||||
if (StringUtils.isBlank(hmclHome)) {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||
}
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
String hmclCurrentDir = System.getProperty("hmcl.dir");
|
||||
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
|
||||
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
|
||||
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
|
||||
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
|
||||
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
|
||||
: CURRENT_DIRECTORY.resolve(".hmcl");
|
||||
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||
|
||||
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
|
||||
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
|
||||
? Path.of(hmclDependencies).toAbsolutePath().normalize()
|
||||
: HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
|
||||
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
Optional<Path> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile.get());
|
||||
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
}
|
||||
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setGraphicsBackend(vs.getGraphicsBackend())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
@@ -514,7 +515,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return FileUtils.isNameValid(id);
|
||||
return FileUtils.isNameValidForJar(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,7 +72,6 @@ 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;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
|
||||
|
||||
public final class LauncherHelper {
|
||||
|
||||
@@ -173,27 +172,27 @@ public final class LauncherHelper {
|
||||
}
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
@@ -232,6 +231,7 @@ public final class LauncherHelper {
|
||||
if (quickPlayOption != null) {
|
||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
@@ -624,7 +624,7 @@ public final class LauncherHelper {
|
||||
break;
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
|
||||
&& isCompatibleWithX86Java()) {
|
||||
&& Platform.isSupportedTranslationX86_64()) {
|
||||
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -31,7 +31,6 @@ public final class Log {
|
||||
|
||||
private final String log;
|
||||
private Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log) {
|
||||
this.log = log;
|
||||
@@ -57,14 +56,6 @@ public final class Log {
|
||||
return level;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return log;
|
||||
|
||||
@@ -51,7 +51,10 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -167,7 +170,7 @@ public final class ModpackHelper {
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static boolean isExternalGameNameConflicts(String name) {
|
||||
@@ -211,16 +214,16 @@ public final class ModpackHelper {
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
@@ -228,7 +231,7 @@ public final class ModpackHelper {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
@@ -163,6 +163,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!future.isDone())
|
||||
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
|
||||
stop();
|
||||
}
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||
|
||||
@@ -99,7 +99,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
|
||||
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
|
||||
for (Path file : stream) {
|
||||
try {
|
||||
@@ -117,8 +117,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
|
||||
list.add(executable);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -131,28 +130,23 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<JavaRuntime> getAllJava(Platform platform) {
|
||||
public Collection<Path> 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);
|
||||
}
|
||||
ArrayList<Path> list = new ArrayList<>();
|
||||
|
||||
getAllJava(list, platform, platformRoot);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
Path javaDir = getJavaDir(platform, gameJavaVersion);
|
||||
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
|
||||
|
||||
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
|
||||
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
|
||||
Path executable;
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
@@ -165,16 +159,16 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
JavaInfo info;
|
||||
if (JavaManager.isCompatible(platform))
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
info = JavaInfoUtils.fromExecutable(executable);
|
||||
else
|
||||
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
|
||||
info = new JavaInfo(platform, result.download().version().name(), null);
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>();
|
||||
update.put("provider", "mojang");
|
||||
update.put("component", gameJavaVersion.component());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles.getFiles().forEach((path, file) -> {
|
||||
result.remoteFiles().getFiles().forEach((path, file) -> {
|
||||
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
|
||||
if (downloadInfo != null) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
@@ -27,7 +28,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -39,35 +39,10 @@ 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 {
|
||||
public static @NotNull JavaInfo fromExecutable(Path executable) 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");
|
||||
}
|
||||
@@ -94,7 +69,6 @@ public final class JavaInfoUtils {
|
||||
? architecture
|
||||
: Architecture.SYSTEM_ARCH);
|
||||
|
||||
|
||||
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
@@ -103,14 +77,9 @@ public final class JavaInfoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@JsonSerializable
|
||||
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
|
||||
@SerializedName("java.version") String javaVersion,
|
||||
@SerializedName("java.vendor") String javaVendor) {
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -30,7 +32,9 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
@@ -40,9 +44,14 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -55,6 +64,16 @@ public final class JavaManager {
|
||||
private JavaManager() {
|
||||
}
|
||||
|
||||
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
|
||||
"Java",
|
||||
"BellSoft",
|
||||
"AdoptOpenJDK",
|
||||
"Zulu",
|
||||
"Microsoft",
|
||||
"Eclipse Foundation",
|
||||
"Semeru"
|
||||
};
|
||||
|
||||
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"));
|
||||
|
||||
@@ -103,16 +122,27 @@ public final class JavaManager {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
// Windows x86-64 platform is compatible with x86 programs
|
||||
return architecture == Architecture.X86;
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
|
||||
// Since Windows 10 Build 21277, Windows Arm64 has been compatible with x86-64 programs via translation
|
||||
if (architecture == Architecture.X86_64 && Platform.isSupportedTranslationX86_64())
|
||||
return true;
|
||||
|
||||
// Windows Arm64 is compatible with x86 programs via translation
|
||||
if (architecture == Architecture.X86)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case LINUX:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
return architecture == Architecture.X86;
|
||||
break;
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
|
||||
return architecture == Architecture.X86_64;
|
||||
break;
|
||||
}
|
||||
@@ -160,12 +190,12 @@ public final class JavaManager {
|
||||
return javaRuntime;
|
||||
}
|
||||
|
||||
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
|
||||
return JavaRuntime.of(executable, info, false);
|
||||
}
|
||||
|
||||
public static void refresh() {
|
||||
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (result != null) {
|
||||
LATCH.await();
|
||||
allJava = result;
|
||||
@@ -326,7 +356,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
|
||||
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
|
||||
JavaManager.allJava = allJava;
|
||||
LATCH.countDown();
|
||||
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
|
||||
@@ -334,56 +364,56 @@ public final class JavaManager {
|
||||
|
||||
// search java
|
||||
|
||||
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
|
||||
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
|
||||
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
|
||||
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
|
||||
if (useCache)
|
||||
searcher.loadCache();
|
||||
|
||||
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
if (Platform.isSupportedTranslationX86_64())
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
}
|
||||
break;
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
|
||||
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
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");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
|
||||
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
|
||||
}
|
||||
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
|
||||
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
break;
|
||||
case LINUX:
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
break;
|
||||
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"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
|
||||
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
|
||||
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
|
||||
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
|
||||
searcher.tryAddJavaExecutable(Path.of("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
|
||||
// Homebrew
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
|
||||
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
|
||||
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
|
||||
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
|
||||
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
|
||||
for (Path dir : dirs) {
|
||||
searchAllJavaInDirectory(javaRuntimes, dir);
|
||||
searcher.searchAllJavaInDirectory(dir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
|
||||
@@ -397,16 +427,16 @@ public final class JavaManager {
|
||||
// Search Minecraft bundled runtimes
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
|
||||
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
|
||||
|
||||
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(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);
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
|
||||
// Search in PATH.
|
||||
if (System.getenv("PATH") != null) {
|
||||
@@ -420,7 +450,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
@@ -430,16 +460,17 @@ public final class JavaManager {
|
||||
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(path));
|
||||
searcher.tryAddJavaHome(Path.of(path));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
|
||||
|
||||
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
|
||||
|
||||
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
|
||||
searcher.tryAddJavaExecutable(Path.of(javaPath));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path: " + javaPath);
|
||||
}
|
||||
@@ -447,233 +478,406 @@ public final class JavaManager {
|
||||
|
||||
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
|
||||
if (currentJava != null
|
||||
&& !javaRuntimes.containsKey(currentJava.getBinary())
|
||||
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
|
||||
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
|
||||
javaRuntimes.put(currentJava.getBinary(), currentJava);
|
||||
searcher.addResult(currentJava.getBinary(), currentJava);
|
||||
}
|
||||
|
||||
LOG.trace(javaRuntimes.values().stream().sorted()
|
||||
searcher.saveCache();
|
||||
|
||||
LOG.trace(searcher.javaRuntimes.values().stream().sorted()
|
||||
.map(it -> String.format(" - %s %s (%s, %s): %s",
|
||||
it.isJDK() ? "JDK" : "JRE",
|
||||
it.getVersion(),
|
||||
it.getPlatform().getArchitecture().getDisplayName(),
|
||||
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
|
||||
it.getBinary()))
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
|
||||
|
||||
return javaRuntimes;
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
|
||||
return searcher.javaRuntimes;
|
||||
}
|
||||
|
||||
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
|
||||
Path executable = getExecutable(javaHome);
|
||||
if (!Files.isRegularFile(executable)) {
|
||||
return;
|
||||
private static final class Searcher {
|
||||
private final Path cacheFile;
|
||||
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
|
||||
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
|
||||
private final Set<Path> failed = new HashSet<>();
|
||||
private boolean needRefreshCache = false;
|
||||
|
||||
Searcher(Path cacheFile) {
|
||||
this.cacheFile = cacheFile;
|
||||
}
|
||||
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to resolve path " + executable, e);
|
||||
return;
|
||||
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
|
||||
private static final int CACHE_MAJOR_VERSION = 0;
|
||||
private static final int CACHE_MINOR_VERSION = 0;
|
||||
|
||||
private record JavaInfoCache(String key, JavaInfo info) {
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
void loadCache() {
|
||||
if (Files.notExists(cacheFile))
|
||||
return;
|
||||
|
||||
JavaInfo info = null;
|
||||
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
if (Files.exists(releaseFile)) {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read release file " + releaseFile, e);
|
||||
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
|
||||
JsonElement fileVersion = jsonFile.get("version");
|
||||
|
||||
Matcher matcher;
|
||||
if (fileVersion instanceof JsonPrimitive version
|
||||
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
|
||||
int major = Integer.parseInt(matcher.group("major"));
|
||||
|
||||
String minorString = matcher.group("minor");
|
||||
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
|
||||
|
||||
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
|
||||
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
|
||||
} else
|
||||
throw new IOException("Invalid version JSON: " + fileVersion);
|
||||
|
||||
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
|
||||
|
||||
for (JsonElement element : cachesArray) {
|
||||
try {
|
||||
var obj = (JsonObject) element;
|
||||
|
||||
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
|
||||
String key = obj.getAsJsonPrimitive("key").getAsString();
|
||||
|
||||
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
|
||||
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
|
||||
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
|
||||
|
||||
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
|
||||
|
||||
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
|
||||
infoBuilder.setVendor(vendor.getAsString());
|
||||
|
||||
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid cache: " + element);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.warning("Failed to load cache file: " + cacheFile);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
void saveCache() {
|
||||
if (!needRefreshCache)
|
||||
return;
|
||||
|
||||
needRefreshCache = false;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
FileUtils.saveSafely(cacheFile, output -> {
|
||||
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
|
||||
writer.beginObject();
|
||||
|
||||
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
|
||||
|
||||
writer.name("caches");
|
||||
writer.beginArray();
|
||||
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
|
||||
Path path = entry.getKey();
|
||||
JavaInfoCache cache = entry.getValue();
|
||||
JavaInfo info = cache.info();
|
||||
|
||||
writer.beginObject();
|
||||
|
||||
writer.name("path").value(path.toString());
|
||||
writer.name("key").value(cache.key());
|
||||
|
||||
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
|
||||
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
|
||||
writer.name("java.version").value(info.getVersion());
|
||||
if (info.getVendor() != null)
|
||||
writer.name("java.vendor").value(info.getVendor());
|
||||
|
||||
writer.endObject();
|
||||
}
|
||||
writer.endArray();
|
||||
|
||||
writer.endObject();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to save cache file: " + cacheFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable String createCacheKey(Path realPath) {
|
||||
Path binDir = realPath.getParent();
|
||||
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
|
||||
return null;
|
||||
|
||||
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
|
||||
return null;
|
||||
|
||||
Path javaHome = binDir.getParent();
|
||||
if (javaHome == null)
|
||||
return null;
|
||||
|
||||
Path libDir = javaHome.resolve("lib");
|
||||
if (!Files.isDirectory(libDir))
|
||||
return null;
|
||||
|
||||
BasicFileAttributes launcherAttributes;
|
||||
String releaseHash = null;
|
||||
BasicFileAttributes coreLibsAttributes = null;
|
||||
|
||||
try {
|
||||
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
|
||||
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
if (Files.exists(releaseFile)) {
|
||||
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
|
||||
} else {
|
||||
Path coreLibsFile = libDir.resolve("rt.jar");
|
||||
if (!Files.isRegularFile(coreLibsFile)) {
|
||||
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
|
||||
if (!Files.isRegularFile(coreLibsFile))
|
||||
return null;
|
||||
|
||||
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to create cache key for " + realPath, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
joiner.add("sz:" + launcherAttributes.size());
|
||||
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
|
||||
|
||||
if (releaseHash != null)
|
||||
joiner.add(releaseHash);
|
||||
|
||||
if (coreLibsAttributes != null) {
|
||||
joiner.add("rsz:" + coreLibsAttributes.size());
|
||||
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
|
||||
}
|
||||
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
void addResult(Path realPath, JavaRuntime javaRuntime) {
|
||||
javaRuntimes.put(realPath, javaRuntime);
|
||||
}
|
||||
|
||||
void tryAddJavaHome(Path javaHome) {
|
||||
tryAddJavaExecutable(getExecutable(javaHome));
|
||||
}
|
||||
|
||||
void tryAddJavaExecutable(Path executable) {
|
||||
tryAddJavaExecutable(executable, false);
|
||||
}
|
||||
|
||||
void tryAddJavaExecutable(Path executable, boolean isManaged) {
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable)
|
||||
|| failed.contains(executable)
|
||||
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = createCacheKey(executable);
|
||||
if (cacheKey != null) {
|
||||
JavaInfoCache cache = caches.get(executable);
|
||||
if (cache != null) {
|
||||
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
|
||||
return;
|
||||
} else {
|
||||
caches.remove(executable);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
} else if (caches.remove(executable) != null) {
|
||||
needRefreshCache = true;
|
||||
}
|
||||
|
||||
JavaInfo info;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
failed.add(executable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cacheKey != null) {
|
||||
caches.put(executable, new JavaInfoCache(cacheKey, info));
|
||||
needRefreshCache = true;
|
||||
}
|
||||
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
|
||||
}
|
||||
|
||||
void tryAddJavaInComponentDir(String platform, Path component, boolean verify) {
|
||||
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
|
||||
if (!Files.isRegularFile(sha1File))
|
||||
return;
|
||||
|
||||
Path dir = component.resolve(platform).resolve(component.getFileName());
|
||||
|
||||
if (verify) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
int idx = line.indexOf(" /#//");
|
||||
if (idx <= 0)
|
||||
throw new IOException("Illegal line: " + line);
|
||||
|
||||
Path file = dir.resolve(line.substring(0, idx));
|
||||
|
||||
// Should we check the sha1 of files? This will take a lot of time.
|
||||
if (Files.notExists(file))
|
||||
throw new NoSuchFileException(file.toAbsolutePath().toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to verify Java in " + component, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(macPath);
|
||||
return;
|
||||
} else
|
||||
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
|
||||
}
|
||||
|
||||
tryAddJavaHome(dir);
|
||||
}
|
||||
|
||||
void searchAllJavaInRepository(Platform platform) {
|
||||
for (Path java : REPOSITORY.getAllJava(platform)) {
|
||||
tryAddJavaExecutable(java, true);
|
||||
}
|
||||
|
||||
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
tryAddJavaExecutable(java, true);
|
||||
}
|
||||
|
||||
if (platform.os() == OperatingSystem.MACOS) {
|
||||
// In the past, we used 'osx' as the checked name for macOS
|
||||
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
|
||||
searchAllJavaInDirectory(platformRoot);
|
||||
}
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform()))
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
void searchAllOfficialJava(Path directory, boolean verify) {
|
||||
if (!Files.isDirectory(directory))
|
||||
return;
|
||||
// Examples:
|
||||
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
|
||||
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
|
||||
|
||||
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
|
||||
if (javaPlatform != null) {
|
||||
searchAllOfficialJava(directory, javaPlatform, verify);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (Platform.isSupportedTranslationX86_64()) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
}
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64()) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaInfo info = null;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform())) {
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
|
||||
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
|
||||
if (!Files.isRegularFile(sha1File))
|
||||
return;
|
||||
|
||||
Path dir = component.resolve(platform).resolve(component.getFileName());
|
||||
|
||||
if (verify) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
int idx = line.indexOf(" /#//");
|
||||
if (idx <= 0)
|
||||
throw new IOException("Illegal line: " + line);
|
||||
|
||||
Path file = dir.resolve(line.substring(0, idx));
|
||||
|
||||
// Should we check the sha1 of files? This will take a lot of time.
|
||||
if (Files.notExists(file))
|
||||
throw new NoSuchFileException(file.toAbsolutePath().toString());
|
||||
void searchAllOfficialJava(Path directory, String platform, boolean verify) {
|
||||
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
|
||||
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
|
||||
for (Path component : dir) {
|
||||
tryAddJavaInComponentDir(platform, component, verify);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to verify Java in " + component, e);
|
||||
return;
|
||||
LOG.warning("Failed to list java-runtime directory " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(javaRuntimes, macPath);
|
||||
void searchAllJavaInDirectory(Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
} else
|
||||
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
|
||||
}
|
||||
}
|
||||
|
||||
tryAddJavaHome(javaRuntimes, dir);
|
||||
}
|
||||
|
||||
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
|
||||
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) {
|
||||
if (!Files.isDirectory(directory))
|
||||
return;
|
||||
// Examples:
|
||||
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
|
||||
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
|
||||
|
||||
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
|
||||
if (javaPlatform != null) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(subDir);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
|
||||
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
|
||||
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
|
||||
for (Path component : dir) {
|
||||
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
|
||||
void searchJavaInProgramFiles(String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Path.of(programFiles);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list java-runtime directory " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(javaRuntimes, subDir);
|
||||
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
|
||||
searchAllJavaInDirectory(path.resolve(vendor));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Paths.get(programFiles);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
|
||||
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
|
||||
void searchJavaInMacJavaVirtualMachines(Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(subDir.resolve("Contents/Home"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Windows Registry Support ====
|
||||
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null)
|
||||
return;
|
||||
// ==== Windows Registry Support ====
|
||||
void queryJavaInRegistryKey(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;
|
||||
Object home = reg.queryValue(hkey, java, "JavaHome");
|
||||
if (home instanceof String) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
for (String java : reg.querySubKeys(hkey, location)) {
|
||||
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
|
||||
continue;
|
||||
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
|
||||
try {
|
||||
tryAddJavaHome(Path.of(home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -468,7 +468,9 @@ public final class Accounts {
|
||||
}
|
||||
} else if (exception instanceof MicrosoftService.XBox400Exception) {
|
||||
return i18n("account.methods.microsoft.error.wrong_verify_method");
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionLicenseNotFoundException) {
|
||||
return i18n("account.methods.microsoft.error.no_license");
|
||||
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionProfileNotFoundException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
|
||||
@@ -567,6 +567,21 @@ public final class Config extends ObservableSetting {
|
||||
this.versionListSource.set(versionListSource);
|
||||
}
|
||||
|
||||
@SerializedName("defaultAddonSource")
|
||||
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
|
||||
|
||||
public StringProperty defaultAddonSourceProperty() {
|
||||
return defaultAddonSource;
|
||||
}
|
||||
|
||||
public String getDefaultAddonSource() {
|
||||
return defaultAddonSource.get();
|
||||
}
|
||||
|
||||
public void setDefaultAddonSource(String defaultAddonSource) {
|
||||
this.defaultAddonSource.set(defaultAddonSource);
|
||||
}
|
||||
|
||||
@SerializedName("hasProxy")
|
||||
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public enum VersionIconType {
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case NEO_FORGE -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
|
||||
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
processPriorityProperty.set(processPriority);
|
||||
}
|
||||
|
||||
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
|
||||
|
||||
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
|
||||
return graphicsBackend;
|
||||
}
|
||||
|
||||
public GraphicsAPI getGraphicsBackend() {
|
||||
return graphicsBackendProperty().get();
|
||||
}
|
||||
|
||||
public void setGraphicsBackend(GraphicsAPI api) {
|
||||
graphicsBackendProperty().set(api);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
|
||||
|
||||
public Renderer getRenderer() {
|
||||
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
obj.addProperty("java", java);
|
||||
|
||||
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
|
||||
obj.addProperty("renderer", src.getRenderer().name());
|
||||
if (src.getRenderer() == Renderer.LLVMPIPE)
|
||||
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
|
||||
obj.addProperty("useSoftwareRenderer", true);
|
||||
|
||||
return obj;
|
||||
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
|
||||
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
|
||||
.map(Renderer::of).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
|
||||
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
|
||||
.flatMap(name -> {
|
||||
try {
|
||||
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -71,9 +71,15 @@ public final class TerracottaBundle {
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
super.withResult(success);
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
delegate.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
context.checkCancellation();
|
||||
@@ -113,7 +119,7 @@ public final class TerracottaBundle {
|
||||
throw new ArtifactMalformedException(String.format("Expecting %s file in terracotta bundle.", file));
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
|
||||
try (
|
||||
InputStream is = tree.getInputStream(archive);
|
||||
OutputStream os = new DigestOutputStream(Files.newOutputStream(path), digest)
|
||||
@@ -122,8 +128,8 @@ public final class TerracottaBundle {
|
||||
}
|
||||
|
||||
String hash = HexFormat.of().formatHex(digest.digest());
|
||||
if (!check.getChecksum().equalsIgnoreCase(hash)) {
|
||||
throw new ChecksumMismatchException(check.getAlgorithm(), check.getChecksum(), hash);
|
||||
if (!check.checksum().equalsIgnoreCase(hash)) {
|
||||
throw new ChecksumMismatchException(check.algorithm(), check.checksum(), hash);
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
@@ -172,7 +178,7 @@ public final class TerracottaBundle {
|
||||
return false;
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
|
||||
try (InputStream is = new DigestInputStream(Files.newInputStream(path), digest)) {
|
||||
int n;
|
||||
while ((n = is.read(buffer)) >= 0) {
|
||||
@@ -182,7 +188,7 @@ public final class TerracottaBundle {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.getChecksum())) {
|
||||
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.checksum())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.ui.MacOSNativeUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -233,6 +234,11 @@ public final class Themes {
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) {
|
||||
MacOSNativeUtils.setAppearance(darkModeProperty().get());
|
||||
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -105,9 +106,14 @@ public final class Controllers {
|
||||
GameListPage gameListPage = new GameListPage();
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
Path modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
FXUtils.applyDragListener(gameListPage, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> {
|
||||
Path file = files.get(0);
|
||||
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Versions.installFromJson(Profiles.getSelectedProfile(), file);
|
||||
}
|
||||
});
|
||||
return gameListPage;
|
||||
});
|
||||
@@ -208,6 +214,21 @@ public final class Controllers {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
public static void saveWindowStates() {
|
||||
if (stageX != null) {
|
||||
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
|
||||
}
|
||||
if (stageY != null) {
|
||||
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
|
||||
}
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
config().setWidth(stageWidth.get());
|
||||
}
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
stageSizeChangeListener = null;
|
||||
if (stageX != null) {
|
||||
@@ -370,7 +391,7 @@ public final class Controllers {
|
||||
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
|
||||
continueAction.run();
|
||||
} 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 &&
|
||||
@@ -574,7 +595,7 @@ public final class Controllers {
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, @NotNull TaskCancellationAction onCancel) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setExecutor(executor);
|
||||
@@ -582,7 +603,7 @@ public final class Controllers {
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, @NotNull TaskCancellationAction onCancel) {
|
||||
TaskExecutor executor = task.executor();
|
||||
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
|
||||
executor.start();
|
||||
|
||||
@@ -62,6 +62,7 @@ import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -235,8 +236,8 @@ public final class FXUtils {
|
||||
|
||||
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", "apng"
|
||||
public static final List<String> IMAGE_EXTENSIONS = List.of(
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
|
||||
);
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
@@ -1166,12 +1167,10 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static Image loadImage(String url) throws Exception {
|
||||
URI uri = NetworkUtils.toURI(url);
|
||||
|
||||
URLConnection connection = NetworkUtils.createConnection(uri);
|
||||
if (connection instanceof HttpURLConnection)
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
public static Image loadImage(WebURL url) throws Exception {
|
||||
URLConnection connection = NetworkUtils.createConnection(url);
|
||||
if (connection instanceof HttpURLConnection httpConnection)
|
||||
connection = NetworkUtils.resolveConnection(httpConnection);
|
||||
|
||||
try (BufferedInputStream input = new BufferedInputStream(connection.getInputStream())) {
|
||||
String contentType = Objects.requireNonNull(connection.getContentType(), "");
|
||||
|
||||
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class HTMLRenderer {
|
||||
private static final String INDENT = " ";
|
||||
private static URI resolveLink(Node linkNode) {
|
||||
String href = linkNode.absUrl("href");
|
||||
if (href.isEmpty())
|
||||
@@ -57,6 +58,7 @@ public final class HTMLRenderer {
|
||||
private boolean underline;
|
||||
private boolean strike;
|
||||
private boolean highlight;
|
||||
private int indentLevel;
|
||||
private String headerLevel;
|
||||
private Node hyperlink;
|
||||
|
||||
@@ -218,7 +220,7 @@ public final class HTMLRenderer {
|
||||
appendImage(node);
|
||||
break;
|
||||
case "li":
|
||||
appendText("\n \u2022 ");
|
||||
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
|
||||
break;
|
||||
case "dt":
|
||||
appendText(" ");
|
||||
@@ -237,11 +239,14 @@ public final class HTMLRenderer {
|
||||
}
|
||||
|
||||
if (node.childNodeSize() > 0) {
|
||||
boolean isLiNode = "li".equals(name);
|
||||
if (isLiNode) indentLevel++;
|
||||
pushNode(node);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
if (isLiNode) indentLevel--;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
@@ -39,7 +38,6 @@ import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -170,6 +168,7 @@ public final class LogWindow extends Stage {
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
listView.getProperties().put("no-smooth-scrolling", true);
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
@@ -260,9 +259,6 @@ public final class LogWindow extends Stage {
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||
private final JFXSnackbar snackbar = new JFXSnackbar();
|
||||
|
||||
LogWindowSkin(LogWindowImpl control) {
|
||||
@@ -308,6 +304,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
{
|
||||
ListView<Log> listView = control.listView;
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
listView.getItems().addListener((InvalidationListener) observable -> {
|
||||
if (!listView.getItems().isEmpty() && control.autoScroll.get())
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
@@ -317,7 +314,6 @@ public final class LogWindow extends Stage {
|
||||
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
{
|
||||
x.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
@@ -327,32 +323,6 @@ public final class LogWindow extends Stage {
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
if (!event.isControlDown()) {
|
||||
for (ListCell<Log> logListCell : selected) {
|
||||
if (logListCell != this) {
|
||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||
if (logListCell.getItem() != null) {
|
||||
logListCell.getItem().setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected.clear();
|
||||
}
|
||||
|
||||
selected.add(this);
|
||||
pseudoClassStateChanged(SELECTED, true);
|
||||
if (getItem() != null) {
|
||||
getItem().setSelected(true);
|
||||
}
|
||||
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -366,7 +336,6 @@ public final class LogWindow extends Stage {
|
||||
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
@@ -378,10 +347,13 @@ public final class LogWindow extends Stage {
|
||||
|
||||
listView.setOnKeyPressed(event -> {
|
||||
if (event.isControlDown() && event.getCode() == KeyCode.C) {
|
||||
if (listView.getSelectionModel().isEmpty())
|
||||
return;
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (Log item : listView.getItems()) {
|
||||
if (item != null && item.isSelected()) {
|
||||
for (Log item : listView.getSelectionModel().getSelectedItems()) {
|
||||
if (item != null) {
|
||||
if (item.getLog() != null)
|
||||
stringBuilder.append(item.getLog());
|
||||
stringBuilder.append('\n');
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MacOSNativeUtils {
|
||||
|
||||
private static final Pointer nsApp = init();
|
||||
|
||||
private static @Nullable Pointer init() {
|
||||
if (ObjectiveCRuntime.INSTANCE == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
|
||||
Pointer nsApplication = objc.objc_getClass("NSApplication");
|
||||
if (!isNull(nsApplication)) {
|
||||
Pointer sharedSel = objc.sel_registerName("sharedApplication");
|
||||
if (!isNull(sharedSel))
|
||||
return objc.objc_msgSend(nsApplication, sharedSel);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to initialize macOS appearance support", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return nsApp != null;
|
||||
}
|
||||
|
||||
private static boolean isNull(Pointer pointer) {
|
||||
return pointer == null || Pointer.nativeValue(pointer) == 0;
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark) {
|
||||
setAppearance(dark, false);
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark, boolean highContrast) {
|
||||
if (nsApp == null) return;
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
|
||||
Pointer nsAppearance = objc.objc_getClass("NSAppearance");
|
||||
if (isNull(nsAppearance))
|
||||
return;
|
||||
|
||||
Pointer namedSel = objc.sel_registerName("appearanceNamed:");
|
||||
Pointer nsString = objc.objc_getClass("NSString");
|
||||
if (isNull(nsString)) return;
|
||||
|
||||
Pointer sel = objc.sel_registerName("stringWithUTF8String:");
|
||||
|
||||
String appearanceName;
|
||||
if (highContrast) {
|
||||
appearanceName = dark ? "NSAppearanceNameAccessibilityHighContrastDarkAqua" : "NSAppearanceNameAccessibilityHighContrastAqua";
|
||||
} else {
|
||||
appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua";
|
||||
}
|
||||
|
||||
Pointer appearanceNamePtr = objc.objc_msgSend(nsString, sel, appearanceName);
|
||||
if (isNull(appearanceNamePtr)) return;
|
||||
|
||||
Pointer appearance = objc.objc_msgSend(nsAppearance, namedSel, appearanceNamePtr);
|
||||
if (isNull(appearance)) return;
|
||||
|
||||
Pointer setSel = objc.sel_registerName("setAppearance:");
|
||||
objc.objc_msgSend(nsApp, setSel, appearance);
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("Failed to set macOS appearance", t);
|
||||
}
|
||||
}
|
||||
|
||||
private MacOSNativeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public enum SVG {
|
||||
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
|
||||
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
|
||||
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
|
||||
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 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"),
|
||||
|
||||
@@ -50,10 +50,10 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new JFXSpinner());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
String url = CHANGELOG_URL + remoteVersion.channel().channelName + ".html";
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.getVersion());
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.version());
|
||||
VersionNumber currentVersion = VersionNumber.asVersion(Metadata.VERSION);
|
||||
if (targetVersion.compareTo(currentVersion) <= 0)
|
||||
// Downgrade update, no need to display changelog
|
||||
|
||||
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (skinImg.isError()) {
|
||||
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
|
||||
}
|
||||
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
|
||||
throw new InvalidSkinException("Invalid skin size");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
GridPane.setRowIndex(lblServerName, 1);
|
||||
|
||||
lblServerWarning = new Label(i18n("account.injector.http"));
|
||||
lblServerWarning.setStyle("-fx-text-fill: red;");
|
||||
lblServerWarning.setStyle("-fx-text-fill: -monet-error;");
|
||||
GridPane.setColumnIndex(lblServerWarning, 0);
|
||||
GridPane.setRowIndex(lblServerWarning, 2);
|
||||
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
|
||||
|
||||
@@ -303,7 +303,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -29,6 +30,7 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
@@ -135,7 +137,9 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
capeSelector.setValue(account.getSkin().getLocalCapePath());
|
||||
}
|
||||
|
||||
skinBinding = FXUtils.observeWeak(() -> {
|
||||
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(1));
|
||||
|
||||
Runnable loadSkin = () -> {
|
||||
getSkin().load(account.getUsername())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
@@ -157,6 +161,23 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
result.getCape() != null ? result.getCape().getImage() : null);
|
||||
}
|
||||
}).start();
|
||||
};
|
||||
|
||||
pauseTransition.setOnFinished(e -> loadSkin.run());
|
||||
|
||||
skinBinding = FXUtils.observeWeak(() -> {
|
||||
Skin.Type selectedType = skinItem.getSelectedData();
|
||||
|
||||
if (selectedType == Skin.Type.CUSTOM_SKIN_LOADER_API) {
|
||||
if (!cslApiField.validate()) {
|
||||
pauseTransition.stop();
|
||||
return;
|
||||
}
|
||||
pauseTransition.playFromStart();
|
||||
} else {
|
||||
pauseTransition.stop();
|
||||
loadSkin.run();
|
||||
}
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -28,6 +30,7 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -43,8 +46,6 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||
boolean noPadding = !sublist.hasComponentPadding();
|
||||
|
||||
this.getStyleClass().add("options-sublist-wrapper");
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
@@ -104,12 +105,26 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
|
||||
if (container == null) {
|
||||
this.container = new VBox();
|
||||
|
||||
if (!noPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
this.container.getStyleClass().add("container");
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
Rectangle rectangle = FXUtils.setOverflowHidden(container);
|
||||
rectangle.getStyleClass().add("overflow-hidden");
|
||||
|
||||
var last = PseudoClass.getPseudoClass("last");
|
||||
|
||||
InvalidationListener updateArc = o -> {
|
||||
if (ComponentSublistWrapper.this.getPseudoClassStates().contains(last)) {
|
||||
rectangle.setArcHeight(4);
|
||||
rectangle.setArcWidth(4);
|
||||
} else {
|
||||
rectangle.setArcHeight(0);
|
||||
rectangle.setArcWidth(0);
|
||||
}
|
||||
};
|
||||
updateArc.invalidated(null);
|
||||
ComponentSublistWrapper.this.getPseudoClassStates().addListener(updateArc);
|
||||
|
||||
container.getChildren().setAll(sublist);
|
||||
ComponentSublistWrapper.this.getChildren().add(container);
|
||||
|
||||
@@ -120,8 +135,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
|
||||
double contentHeight = expanded ? sublist.prefHeight(sublist.getWidth()) : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@@ -30,6 +31,7 @@ import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -38,10 +40,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FileSelector extends HBox {
|
||||
private final StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private String chooserTitle = "";
|
||||
private SelectionMode selectionMode = SelectionMode.FILE;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
|
||||
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
|
||||
|
||||
public enum SelectionMode {
|
||||
FILE,
|
||||
DIRECTORY,
|
||||
FILE_OR_DIRECTORY
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value.get();
|
||||
}
|
||||
@@ -63,12 +73,12 @@ public class FileSelector extends HBox {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
public SelectionMode getSelectionMode() {
|
||||
return selectionMode;
|
||||
}
|
||||
|
||||
public FileSelector setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
public FileSelector setSelectionMode(SelectionMode selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -80,26 +90,20 @@ public class FileSelector extends HBox {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
FXUtils.bindString(customField, valueProperty());
|
||||
|
||||
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
|
||||
selectButton.setOnAction(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
switch (selectionMode) {
|
||||
case FILE -> openFileChooser(customField);
|
||||
case DIRECTORY -> openDirectoryChooser(customField);
|
||||
case FILE_OR_DIRECTORY -> {
|
||||
PopupMenu selectPopupMenu = new PopupMenu();
|
||||
JFXPopup selectModePopup = new JFXPopup(selectPopupMenu);
|
||||
|
||||
selectPopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.FILE_OPEN, i18n("selector.choose_file"), () -> openFileChooser(customField), selectModePopup),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("selector.choose_directory"), () -> openDirectoryChooser(customField), selectModePopup)
|
||||
);
|
||||
|
||||
selectModePopup.show(selectButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -selectButton.getWidth(), 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -108,4 +112,27 @@ public class FileSelector extends HBox {
|
||||
setSpacing(3);
|
||||
getChildren().addAll(customField, selectButton);
|
||||
}
|
||||
|
||||
private void openFileChooser(JFXTextField customField) {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDirectoryChooser(JFXTextField customField) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.NumberBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
@@ -26,6 +32,10 @@ import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
|
||||
// Referenced in root.css
|
||||
@@ -36,6 +46,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
private Rectangle track = new Rectangle();
|
||||
private Rectangle thumb = new Rectangle();
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
|
||||
private ChangeListener<Boolean> thumbHoverListener;
|
||||
private Animation thumbHoverAnimation;
|
||||
|
||||
public FloatScrollBarSkin(final ScrollBar scrollBar) {
|
||||
this.scrollBar = scrollBar;
|
||||
scrollBar.setPrefHeight(1e-18);
|
||||
@@ -45,8 +59,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
Point2D dragStart;
|
||||
double preDragThumbPos;
|
||||
|
||||
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
|
||||
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
|
||||
final NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
|
||||
final NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
|
||||
|
||||
{
|
||||
// Children are added unmanaged because for some reason the height of the bar keeps changing
|
||||
@@ -63,7 +77,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
|
||||
setup();
|
||||
|
||||
|
||||
thumb.setOnMousePressed(me -> {
|
||||
if (me.isSynthesized()) {
|
||||
// touch-screen events handled by Scroll handler
|
||||
@@ -128,14 +141,18 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
track.widthProperty().unbind();
|
||||
track.heightProperty().unbind();
|
||||
|
||||
double offset = 4;
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
track.relocate(0, -8);
|
||||
track.widthProperty().bind(scrollBar.widthProperty());
|
||||
track.setHeight(8);
|
||||
track.relocate(offset, -6.0);
|
||||
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
|
||||
track.widthProperty().bind(trackWidth);
|
||||
track.setHeight(6.0);
|
||||
} else {
|
||||
track.relocate(-8, 0);
|
||||
track.setWidth(8);
|
||||
track.heightProperty().bind(scrollBar.heightProperty());
|
||||
track.relocate(-6.0, offset);
|
||||
track.setWidth(6.0);
|
||||
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
|
||||
track.heightProperty().bind(trackHeight);
|
||||
}
|
||||
|
||||
thumb.xProperty().unbind();
|
||||
@@ -144,15 +161,37 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
thumb.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
thumb.relocate(0, -8);
|
||||
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
|
||||
thumb.setHeight(8);
|
||||
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
|
||||
thumb.relocate(0, -6.0);
|
||||
thumb.setHeight(6.0);
|
||||
|
||||
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
|
||||
|
||||
thumb.widthProperty().bind(Bindings.min(trackWidth,
|
||||
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackWidth))));
|
||||
|
||||
thumb.xProperty().bind(
|
||||
Bindings.add(offset,
|
||||
Bindings.multiply(
|
||||
Bindings.max(0, Bindings.subtract(trackWidth, thumb.widthProperty())),
|
||||
position)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
thumb.relocate(-8, 0);
|
||||
thumb.setWidth(8);
|
||||
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
|
||||
thumb.relocate(-6.0, 0);
|
||||
thumb.setWidth(6.0);
|
||||
|
||||
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
|
||||
|
||||
thumb.heightProperty().bind(Bindings.min(trackHeight,
|
||||
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackHeight))));
|
||||
|
||||
thumb.yProperty().bind(
|
||||
Bindings.add(offset,
|
||||
Bindings.multiply(
|
||||
Bindings.max(0, Bindings.subtract(trackHeight, thumb.heightProperty())),
|
||||
position)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +201,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 8;
|
||||
return 6.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,13 +210,46 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 8;
|
||||
return 6.0;
|
||||
}
|
||||
};
|
||||
|
||||
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
|
||||
if (thumbHoverAnimation != null) {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
}
|
||||
|
||||
double targetOpacity = newValue ? 1.0 : 0.5;
|
||||
double currentOpacity = thumb.getOpacity();
|
||||
|
||||
double opacityAdjustment = targetOpacity - currentOpacity;
|
||||
if (Math.abs(opacityAdjustment) < 0.001) {
|
||||
if (opacityAdjustment != 0)
|
||||
thumb.setOpacity(targetOpacity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled() && JFXNodeUtils.isTreeVisible(thumb)) {
|
||||
thumbHoverAnimation = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(thumb.opacityProperty(), currentOpacity)),
|
||||
new KeyFrame(Motion.SHORT2, new KeyValue(thumb.opacityProperty(), targetOpacity))
|
||||
);
|
||||
thumbHoverAnimation.play();
|
||||
} else {
|
||||
thumb.setOpacity(targetOpacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (thumbHoverAnimation != null) {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
}
|
||||
thumbHoverListener = null;
|
||||
scrollBar = null;
|
||||
group = null;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -56,6 +57,7 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
||||
this.setBody(new VBox(textField));
|
||||
|
||||
lblCreationWarning = new Label();
|
||||
lblCreationWarning.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
acceptPane = new SpinnerPane();
|
||||
acceptPane.getStyleClass().add("small-spinner-pane");
|
||||
|
||||
@@ -20,14 +20,12 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -35,6 +33,8 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
@@ -43,24 +43,22 @@ import java.util.function.Function;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
|
||||
/// @author Glavo
|
||||
public class LineSelectButton<T> extends LineButton {
|
||||
public class LineSelectButton<T extends @UnknownNullability Object> extends LineButton {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private JFXPopup popup;
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private ObservableList<Node> popupItems; // keep a reference
|
||||
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
InvalidationListener updateTrailingText = observable -> {
|
||||
Function<T, String> converter = getConverter();
|
||||
T value = getValue();
|
||||
if (value != null) {
|
||||
Function<T, String> converter = getConverter();
|
||||
setTrailingText(converter != null ? converter.apply(value) : value.toString());
|
||||
} else {
|
||||
setTrailingText(null);
|
||||
}
|
||||
setTrailingText(converter != null ? converter.apply(value) : Objects.toString(value, ""));
|
||||
};
|
||||
converterProperty().addListener(updateTrailingText);
|
||||
valueProperty().addListener(updateTrailingText);
|
||||
@@ -87,15 +85,12 @@ public class LineSelectButton<T> extends LineButton {
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
itemTitleLabel.getStyleClass().add("title-label");
|
||||
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (item == null)
|
||||
return "";
|
||||
|
||||
Function<T, String> converter = getConverter();
|
||||
return converter != null ? converter.apply(item) : Objects.toString(item, "");
|
||||
}, converterProperty()));
|
||||
@@ -170,10 +165,14 @@ public class LineSelectButton<T> extends LineButton {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
public void setConverter(Function<T, String> value) {
|
||||
public void setConverter(Function<T, String> value) { // TODO: rename
|
||||
converterProperty().set(value);
|
||||
}
|
||||
|
||||
public void setNullSafeConverter(Function<@NotNull T, String> value) {
|
||||
converterProperty().set(it -> it != null ? value.apply(it) : "");
|
||||
}
|
||||
|
||||
private ObjectProperty<Function<T, String>> descriptionConverter;
|
||||
|
||||
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
|
||||
|
||||
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
|
||||
public class MenuSeparator extends StackPane {
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
|
||||
Rectangle rect = new Rectangle();
|
||||
rect.widthProperty().bind(widthProperty().add(-14));
|
||||
rect.setHeight(1);
|
||||
rect.setFill(Color.GRAY);
|
||||
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
|
||||
maxHeightProperty().set(10);
|
||||
setPadding(new Insets(3));
|
||||
getChildren().setAll(rect);
|
||||
|
||||
@@ -55,7 +55,8 @@ public final class MultiFileItem<T> extends VBox {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public MultiFileItem() {
|
||||
setPadding(new Insets(0, 0, 10, 0));
|
||||
this.getStyleClass().add("multi-file-item");
|
||||
|
||||
setSpacing(8);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
@@ -266,8 +267,8 @@ public final class MultiFileItem<T> extends VBox {
|
||||
selector.setValue(value);
|
||||
}
|
||||
|
||||
public FileOption<T> setDirectory(boolean directory) {
|
||||
selector.setDirectory(directory);
|
||||
public FileOption<T> setSelectionMode(FileSelector.SelectionMode selectionMode) {
|
||||
selector.setSelectionMode(selectionMode);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,12 +81,11 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
|
||||
setCancel(cancel);
|
||||
|
||||
btnCancel.setDisable(onCancel.getCancellationAction() == null);
|
||||
btnCancel.setOnAction(e -> {
|
||||
if (onCancel.getCancellationAction() != null) {
|
||||
if (executor != null)
|
||||
executor.cancel();
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
}
|
||||
if (executor != null)
|
||||
executor.cancel();
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
});
|
||||
|
||||
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
|
||||
|
||||
@@ -30,7 +30,6 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
@@ -147,6 +146,10 @@ public class TwoLineListItem extends VBox {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
public HBox getFirstLine() {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
@@ -168,11 +171,23 @@ public class TwoLineListItem extends VBox {
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
scrollPane.setMinSize(0, 0);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
|
||||
var expectedHeight = Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
if (tags.isEmpty()) return 0.0;
|
||||
double h = tagsBox.prefHeight(-1);
|
||||
return h > 0 ? h : Double.NaN;
|
||||
},
|
||||
tags, tagsBox.heightProperty()
|
||||
);
|
||||
|
||||
scrollPane.minHeightProperty().bind(expectedHeight);
|
||||
scrollPane.prefHeightProperty().bind(expectedHeight);
|
||||
scrollPane.maxHeightProperty().bind(expectedHeight);
|
||||
firstLine.getChildren().setAll(lblTitle, scrollPane);
|
||||
|
||||
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
|
||||
|
||||
@@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -54,12 +54,7 @@ public class URLValidator extends ValidatorBase {
|
||||
if (StringUtils.isBlank(textField.getText()))
|
||||
hasErrors.set(!nullable);
|
||||
else {
|
||||
try {
|
||||
NetworkUtils.toURI(textField.getText());
|
||||
hasErrors.set(false);
|
||||
} catch (IllegalArgumentException e) {
|
||||
hasErrors.set(true);
|
||||
}
|
||||
hasErrors.set(WebURL.tryParseBrowserInput(textField.getText()) == null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
@@ -205,7 +206,7 @@ public class DecoratorController {
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null) {
|
||||
try {
|
||||
image = FXUtils.loadImage(backgroundImageUrl);
|
||||
image = FXUtils.loadImage(WebURL.parseBrowserInput(backgroundImageUrl));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Couldn't load background image", e);
|
||||
}
|
||||
|
||||
@@ -274,6 +274,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canBack) {
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(backNavButton);
|
||||
backNavButton.setFocusTraversable(false);
|
||||
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
@@ -285,6 +286,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canClose) {
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(closeNavButton);
|
||||
closeNavButton.setFocusTraversable(false);
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
@@ -349,13 +351,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
skinnable.forbidDraggingWindow(refreshNavButton);
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
|
||||
separator.heightProperty().bind(navBar.heightProperty());
|
||||
separator.setFill(Color.GRAY);
|
||||
|
||||
navRight.getChildren().setAll(refreshNavButton, separator);
|
||||
navRight.getChildren().setAll(refreshNavButton);
|
||||
navBar.setRight(navRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,18 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
|
||||
@@ -134,6 +139,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
|
||||
Set<String> existingFiles;
|
||||
|
||||
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
|
||||
existingFiles = list.map(Path::getFileName)
|
||||
.map(Path::toString)
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
|
||||
existingFiles = Set.of();
|
||||
}
|
||||
|
||||
Set<String> finalExistingFiles = existingFiles;
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
@@ -153,7 +171,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
handler.resolve();
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValidForJar), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
graphic.setMouseTransparent(true);
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_FORWARD.getPath());
|
||||
SVGPath arrow = SVG.ARROW_FORWARD.createIcon();
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
|
||||
@@ -423,7 +423,11 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
if (control.versions.isEmpty()) {
|
||||
transitionPane.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
} else {
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
}
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
@@ -214,7 +214,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
vs.isFullscreen(),
|
||||
vs.getWidth(),
|
||||
vs.getHeight(),
|
||||
vs.getMaxMemory(),
|
||||
null,
|
||||
exportInfo.getMinMemory(),
|
||||
vs.isShowLogs(),
|
||||
/* showConsoleOnError */ true,
|
||||
|
||||
@@ -316,7 +316,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
|
||||
serversSelectButton.setTitle(i18n("account.injector.server"));
|
||||
serversSelectButton.setConverter(AuthlibInjectorServer::getName);
|
||||
serversSelectButton.setNullSafeConverter(AuthlibInjectorServer::getName);
|
||||
serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
|
||||
serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
|
||||
|
||||
|
||||
@@ -17,11 +17,21 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.girod.javafx.svgimage.LoaderParameters;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.girod.javafx.svgimage.ScaleQuality;
|
||||
import org.glavo.webp.WebPImage;
|
||||
import org.glavo.webp.WebPImageLoadOptions;
|
||||
import org.glavo.webp.javafx.WebPFXImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
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;
|
||||
@@ -30,16 +40,13 @@ 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.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -61,16 +68,48 @@ public final class ImageUtils {
|
||||
};
|
||||
|
||||
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();
|
||||
var options = new WebPImageLoadOptions(requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
return new WebPFXImage(WebPImage.read(input, options));
|
||||
};
|
||||
|
||||
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
LoaderParameters parameters = new LoaderParameters();
|
||||
parameters.autoStartAnimations = false;
|
||||
|
||||
SVGImage image;
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
image = SVGLoader.load(content, parameters);
|
||||
} else {
|
||||
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
|
||||
image = CompletableFuture.supplyAsync(
|
||||
() -> SVGLoader.load(content, parameters),
|
||||
Schedulers.javafx()
|
||||
).get();
|
||||
}
|
||||
|
||||
if (image == null)
|
||||
throw new IOException("Failed to load SVG image");
|
||||
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
};
|
||||
|
||||
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
@@ -136,11 +175,13 @@ public final class ImageUtils {
|
||||
|
||||
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
|
||||
"webp", WEBP,
|
||||
"svg", SVG,
|
||||
"apng", APNG
|
||||
);
|
||||
|
||||
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
|
||||
"image/webp", WEBP,
|
||||
"image/svg+xml", SVG,
|
||||
"image/apng", APNG
|
||||
);
|
||||
|
||||
@@ -165,6 +206,14 @@ public final class ImageUtils {
|
||||
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
|
||||
}
|
||||
|
||||
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
// This is currently a simple check, more complex checks can be considered in the future
|
||||
public static boolean isSVG(byte[] headerBuffer) {
|
||||
return headerBuffer.length > SVG_HEADER.length
|
||||
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
|
||||
}
|
||||
|
||||
private static final byte[] PNG_HEADER = {
|
||||
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
|
||||
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
|
||||
@@ -232,6 +281,8 @@ public final class ImageUtils {
|
||||
return WEBP;
|
||||
if (isApng(headerBuffer))
|
||||
return APNG;
|
||||
if (isSVG(headerBuffer))
|
||||
return SVG;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -406,6 +457,19 @@ public final class ImageUtils {
|
||||
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
|
||||
}
|
||||
|
||||
private static int[] rgbaToArgb(ByteBuffer rgba) {
|
||||
int pixelCount = rgba.remaining() / 4;
|
||||
int[] argb = new int[pixelCount];
|
||||
for (int i = 0; i < pixelCount; i++) {
|
||||
int r = rgba.get() & 0xFF;
|
||||
int g = rgba.get() & 0xFF;
|
||||
int b = rgba.get() & 0xFF;
|
||||
int a = rgba.get() & 0xFF;
|
||||
argb[i] = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
return argb;
|
||||
}
|
||||
|
||||
private ImageUtils() {
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
@@ -30,6 +28,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -38,11 +37,18 @@ import java.io.InputStream;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AboutPage extends StackPane {
|
||||
public final class AboutPage extends SpinnerPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public AboutPage() {
|
||||
VBox content = new VBox();
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setContent(scrollPane);
|
||||
|
||||
ComponentList about = new ComponentList();
|
||||
{
|
||||
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
|
||||
@@ -66,7 +72,7 @@ public final class AboutPage extends StackPane {
|
||||
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
|
||||
var copyright = new LineButton();
|
||||
copyright.setLargeTitle(true);
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
@@ -84,27 +90,16 @@ public final class AboutPage extends StackPane {
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
|
||||
VBox content = new VBox(16);
|
||||
content.setPadding(new Insets(10));
|
||||
content.getChildren().setAll(
|
||||
ComponentList.createComponentListTitle(i18n("about")),
|
||||
about,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
|
||||
thanks,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.dependency")),
|
||||
deps,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.legal")),
|
||||
legal
|
||||
);
|
||||
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private static Image loadImage(String url) {
|
||||
|
||||
@@ -27,14 +27,20 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -72,7 +78,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
var versionListSourcePane = new LineSelectButton<String>();
|
||||
versionListSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty().not());
|
||||
versionListSourcePane.setTitle(i18n("settings.launcher.version_list_source"));
|
||||
versionListSourcePane.setConverter(converter);
|
||||
versionListSourcePane.setNullSafeConverter(converter);
|
||||
versionListSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
versionListSourcePane.setItems(DownloadProviders.AUTO_PROVIDERS.keySet());
|
||||
versionListSourcePane.valueProperty().bindBidirectional(config().versionListSourceProperty());
|
||||
@@ -80,21 +86,55 @@ public class DownloadSettingsPage extends StackPane {
|
||||
var downloadSourcePane = new LineSelectButton<String>();
|
||||
downloadSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty());
|
||||
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
|
||||
downloadSourcePane.setConverter(converter);
|
||||
downloadSourcePane.setNullSafeConverter(converter);
|
||||
downloadSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
|
||||
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
|
||||
var defaultAddonSourcePane = new LineSelectButton<String>();
|
||||
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
|
||||
defaultAddonSourcePane.setNullSafeConverter(key -> I18n.i18n("mods." + key));
|
||||
defaultAddonSourcePane.setItems("modrinth", "curseforge");
|
||||
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
|
||||
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
}
|
||||
|
||||
{
|
||||
var downloadList = new ComponentList();
|
||||
|
||||
VBox downloadThreads = new VBox(16);
|
||||
downloadThreads.getStyleClass().add("card-non-transparent");
|
||||
|
||||
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
|
||||
|
||||
{
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
|
||||
.bindBidirectional(config().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocationSublist.subtitleProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnAction(e -> clearCacheDirectory());
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
}
|
||||
|
||||
{
|
||||
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
|
||||
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
|
||||
@@ -147,7 +187,8 @@ public class DownloadSettingsPage extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadThreads);
|
||||
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -324,4 +365,11 @@ public class DownloadSettingsPage extends StackPane {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void clearCacheDirectory() {
|
||||
String commonDirectory = Settings.instance().getCommonDirectory();
|
||||
if (commonDirectory != null) {
|
||||
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
@@ -37,9 +36,7 @@ public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
public FeedbackPage() {
|
||||
VBox content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
@@ -84,8 +81,5 @@ public class FeedbackPage extends SpinnerPane {
|
||||
ComponentList.createComponentListTitle(i18n("contact.feedback")),
|
||||
feedback
|
||||
);
|
||||
|
||||
this.setContent(content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -42,9 +41,7 @@ public class HelpPage extends SpinnerPane {
|
||||
|
||||
public HelpPage() {
|
||||
content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Result;
|
||||
@@ -233,7 +234,6 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
downloadButton.setOnAction(e -> onDownload());
|
||||
downloadButton.getStyleClass().add("dialog-accept");
|
||||
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
@@ -300,6 +300,11 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
remoteVersionBox.getSelectionModel().selectFirst();
|
||||
});
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.getStyleClass().add("small-spinner-pane");
|
||||
spinnerPane.setContent(downloadButton);
|
||||
downloadButtonPane.getChildren().setAll(spinnerPane);
|
||||
|
||||
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
|
||||
remoteVersionBox.setItems(null);
|
||||
packageTypeBox.getItems().clear();
|
||||
@@ -308,11 +313,11 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
warningLabel.setText(null);
|
||||
|
||||
if (list == null || (list.versions != null && list.versions.isEmpty()))
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
spinnerPane.hideSpinner();
|
||||
else if (list.status == DiscoJavaVersionList.Status.LOADING)
|
||||
downloadButtonPane.getChildren().setAll(new JFXSpinner());
|
||||
spinnerPane.showSpinner();
|
||||
else {
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
spinnerPane.hideSpinner();
|
||||
|
||||
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
|
||||
packageTypeBox.getItems().setAll(list.versions.keySet());
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -170,6 +171,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
FXUtils.setLimitHeight(updatePane, 55);
|
||||
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
|
||||
FXUtils.onClicked(updatePane, this::onUpgrade);
|
||||
updatePane.setCursor(Cursor.HAND);
|
||||
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
|
||||
|
||||
{
|
||||
@@ -183,7 +185,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
|
||||
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
|
||||
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.version())));
|
||||
|
||||
hBox.getChildren().setAll(SVG.UPDATE.createIcon(20), prompt);
|
||||
}
|
||||
@@ -279,10 +281,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
if (show && !config().isDisableAutoShowUpdateDialog()
|
||||
&& getLatestVersion() != null
|
||||
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
|
||||
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().version())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().version()), MessageDialogPane.MessageType.INFO)
|
||||
.addAction(i18n("button.view"), () -> {
|
||||
config().setPromptedVersion(getLatestVersion().getVersion());
|
||||
config().setPromptedVersion(getLatestVersion().version());
|
||||
onUpgrade();
|
||||
})
|
||||
.addCancel(null)
|
||||
|
||||
@@ -79,7 +79,7 @@ public class PersonalizationPage extends StackPane {
|
||||
{
|
||||
var brightnessPane = new LineSelectButton<String>();
|
||||
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
|
||||
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness." + name));
|
||||
brightnessPane.setNullSafeConverter(name -> i18n("settings.launcher.brightness." + name));
|
||||
brightnessPane.setItems("auto", "light", "dark");
|
||||
brightnessPane.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
|
||||
@@ -135,6 +135,7 @@ public class PersonalizationPage extends StackPane {
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.background.choose"))
|
||||
.addExtensionFilter(FXUtils.getImageExtensionFilter())
|
||||
.setSelectionMode(FileSelector.SelectionMode.FILE_OR_DIRECTORY)
|
||||
.bindBidirectional(config().backgroundImageProperty()),
|
||||
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
|
||||
.setValidators(new URLValidator(true))
|
||||
@@ -294,7 +295,7 @@ public class PersonalizationPage extends StackPane {
|
||||
var fontAntiAliasingPane = new LineSelectButton<Optional<FontSmoothingType>>();
|
||||
fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing"));
|
||||
fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
fontAntiAliasingPane.setConverter(value ->
|
||||
fontAntiAliasingPane.setNullSafeConverter(value ->
|
||||
value.isPresent()
|
||||
? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT))
|
||||
: i18n("settings.launcher.font.anti_aliasing.auto")
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
@@ -98,7 +99,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage,
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file) || "json".equalsIgnoreCase(FileUtils.getExtension(file)),
|
||||
modpacks -> {
|
||||
Path file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
@@ -113,6 +114,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
|
||||
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Versions.installFromJson(Profiles.getSelectedProfile(), file);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,21 +20,17 @@ package org.jackhuang.hmcl.ui.main;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -61,7 +57,9 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
@@ -78,242 +76,193 @@ public final class SettingsPage extends ScrollPane {
|
||||
public SettingsPage() {
|
||||
this.setFitToWidth(true);
|
||||
|
||||
VBox rootPane = new VBox();
|
||||
VBox rootPane = new VBox(10);
|
||||
rootPane.setPadding(new Insets(10));
|
||||
this.setContent(rootPane);
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
ComponentList settingsPane = new ComponentList();
|
||||
{
|
||||
ComponentList updatePaneList = new ComponentList();
|
||||
{
|
||||
StackPane sponsorPane = new StackPane();
|
||||
sponsorPane.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(sponsorPane, this::onSponsor);
|
||||
sponsorPane.setPadding(new Insets(8, 0, 8, 0));
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
|
||||
ColumnConstraints col = new ColumnConstraints();
|
||||
col.setHgrow(Priority.SOMETIMES);
|
||||
col.setMaxWidth(Double.POSITIVE_INFINITY);
|
||||
|
||||
gridPane.getColumnConstraints().setAll(col);
|
||||
|
||||
RowConstraints row = new RowConstraints();
|
||||
row.setMinHeight(Double.NEGATIVE_INFINITY);
|
||||
row.setValignment(VPos.TOP);
|
||||
row.setVgrow(Priority.SOMETIMES);
|
||||
gridPane.getRowConstraints().setAll(row);
|
||||
|
||||
ObjectProperty<UpdateChannel> updateChannel;
|
||||
{
|
||||
Label label = new Label(i18n("sponsor.hmcl"));
|
||||
label.setWrapText(true);
|
||||
label.setTextAlignment(TextAlignment.JUSTIFY);
|
||||
GridPane.setRowIndex(label, 0);
|
||||
GridPane.setColumnIndex(label, 0);
|
||||
gridPane.getChildren().add(label);
|
||||
}
|
||||
|
||||
sponsorPane.getChildren().setAll(gridPane);
|
||||
settingsPane.getContent().add(sponsorPane);
|
||||
}
|
||||
JFXButton updateButton = FXUtils.newToggleButton4(SVG.UPDATE, 20);
|
||||
updateButton.setOnAction(e -> onUpdate());
|
||||
updateButton.setPadding(Insets.EMPTY);
|
||||
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
|
||||
|
||||
ObjectProperty<UpdateChannel> updateChannel;
|
||||
{
|
||||
var updatePane = new LineSelectButton<UpdateChannel>() {
|
||||
|
||||
JFXButton updateButton = new JFXButton();
|
||||
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
|
||||
updateButton.setOnAction(e -> onUpdate());
|
||||
updateButton.setPadding(Insets.EMPTY);
|
||||
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
|
||||
{
|
||||
getStyleClass().add("update-pane");
|
||||
setNode(IDX_TRAILING, updateButton);
|
||||
}
|
||||
|
||||
var updatePane = new LineSelectButton<UpdateChannel>() {
|
||||
|
||||
{
|
||||
getStyleClass().add("update-pane");
|
||||
setNode(IDX_TRAILING, updateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTrailingTextIndex() {
|
||||
return LineComponent.IDX_TRAILING + 1;
|
||||
}
|
||||
};
|
||||
updateChannel = updatePane.valueProperty();
|
||||
updatePane.setTitle(i18n("update"));
|
||||
updatePane.setValue(UpdateChannel.getChannel());
|
||||
|
||||
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
|
||||
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
|
||||
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
|
||||
|
||||
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
|
||||
|
||||
{
|
||||
updateListener = any -> {
|
||||
boolean outdated = UpdateChecker.isOutdated();
|
||||
|
||||
updateButton.setVisible(outdated);
|
||||
updateButton.setManaged(outdated);
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else {
|
||||
lblUpdateSubProperty.set(i18n("update.latest"));
|
||||
@Override
|
||||
protected int getTrailingTextIndex() {
|
||||
return LineComponent.IDX_TRAILING + 1;
|
||||
}
|
||||
};
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
updateChannel = updatePane.valueProperty();
|
||||
updatePane.setTitle(i18n("update"));
|
||||
updatePane.setValue(UpdateChannel.getChannel());
|
||||
|
||||
updatePane.setNullSafeConverter(channel -> i18n("update.channel." + channel.channelName));
|
||||
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
|
||||
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
|
||||
|
||||
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
|
||||
|
||||
{
|
||||
updateListener = any -> {
|
||||
boolean outdated = UpdateChecker.isOutdated();
|
||||
|
||||
updateButton.setVisible(outdated);
|
||||
updateButton.setManaged(outdated);
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else {
|
||||
lblUpdateSubProperty.set(i18n("update.latest"));
|
||||
}
|
||||
};
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
}
|
||||
|
||||
updatePaneList.getContent().add(updatePane);
|
||||
}
|
||||
|
||||
settingsPane.getContent().add(updatePane);
|
||||
{
|
||||
LineToggleButton previewPane = new LineToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.setSubtitle(i18n("update.preview.subtitle"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
|
||||
InvalidationListener checkUpdateListener = e -> {
|
||||
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
|
||||
};
|
||||
updateChannel.addListener(checkUpdateListener);
|
||||
previewPane.selectedProperty().addListener(checkUpdateListener);
|
||||
|
||||
updatePaneList.getContent().add(previewPane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
|
||||
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
|
||||
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
|
||||
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
|
||||
updatePaneList.getContent().add(disableAutoShowUpdateDialogPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("update")), updatePaneList);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton previewPane = new LineToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.setSubtitle(i18n("update.preview.subtitle"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
ComponentList languagePaneList = new ComponentList();
|
||||
|
||||
InvalidationListener checkUpdateListener = e -> {
|
||||
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
|
||||
};
|
||||
updateChannel.addListener(checkUpdateListener);
|
||||
previewPane.selectedProperty().addListener(checkUpdateListener);
|
||||
{
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
settingsPane.getContent().add(previewPane);
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
chooseLanguagePane.setNullSafeConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
|
||||
|
||||
languagePaneList.getContent().add(chooseLanguagePane);
|
||||
|
||||
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
|
||||
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
|
||||
languagePaneList.getContent().add(disableAutoGameOptionsPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.language")), languagePaneList);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
|
||||
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
|
||||
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
|
||||
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
|
||||
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
|
||||
ComponentList miscPaneList = new ComponentList();
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
|
||||
miscPaneList.getContent().add(disableAprilFools);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton allowAutoAgentPane = new LineToggleButton();
|
||||
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
|
||||
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
|
||||
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
|
||||
|
||||
miscPaneList.getContent().add(allowAutoAgentPane);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.debug"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
debugPane.setLeft(left);
|
||||
|
||||
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
|
||||
openLogFolderButton.setOnAction(e -> openLogFolder());
|
||||
openLogFolderButton.getStyleClass().add("jfx-button-border");
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
|
||||
miscPaneList.getContent().add(debugPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.misc")), miscPaneList);
|
||||
}
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
|
||||
settingsPane.getContent().add(disableAprilFools);
|
||||
}
|
||||
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setDirectory(true)
|
||||
.bindBidirectional(config().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
|
||||
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocationSublist.subtitleProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnAction(e -> clearCacheDirectory());
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
|
||||
settingsPane.getContent().add(fileCommonLocationSublist);
|
||||
}
|
||||
|
||||
{
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
chooseLanguagePane.setConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
|
||||
|
||||
settingsPane.getContent().add(chooseLanguagePane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
|
||||
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
|
||||
settingsPane.getContent().add(disableAutoGameOptionsPane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton allowAutoAgentPane = new LineToggleButton();
|
||||
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
|
||||
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
|
||||
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
|
||||
|
||||
settingsPane.getContent().add(allowAutoAgentPane);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.debug"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
debugPane.setLeft(left);
|
||||
|
||||
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
|
||||
openLogFolderButton.setOnAction(e -> openLogFolder());
|
||||
openLogFolderButton.getStyleClass().add("jfx-button-border");
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
|
||||
settingsPane.getContent().add(debugPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().add(settingsPane);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,15 +401,4 @@ public final class SettingsPage extends ScrollPane {
|
||||
return outputFile;
|
||||
}), Schedulers.io());
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://github.com/HMCL-dev/HMCL");
|
||||
}
|
||||
|
||||
private void clearCacheDirectory() {
|
||||
String commonDirectory = Settings.instance().getCommonDirectory();
|
||||
if (commonDirectory != null) {
|
||||
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class SkinCube extends MeshView {
|
||||
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
|
||||
float startX, float startY, boolean isSlim) {
|
||||
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = half_width,
|
||||
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
|
||||
return new float[]{
|
||||
top_x, top_y, // T0 ---
|
||||
@@ -58,14 +58,14 @@ public class SkinCube extends MeshView {
|
||||
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
|
||||
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
|
||||
// RIGHT
|
||||
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
|
||||
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
|
||||
5, 3, 1, 4, 3, 9, //P5,T3, P1,T4, P3,T9
|
||||
5, 3, 3, 9, 7, 8, //P5,T3, P3,T9, P7,T8
|
||||
// FRONT
|
||||
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
|
||||
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
|
||||
// LEFT
|
||||
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
|
||||
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
|
||||
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
|
||||
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
|
||||
// BACK
|
||||
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
|
||||
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
|
||||
|
||||
@@ -131,14 +131,15 @@ public class SkinMultipleCubes extends Group {
|
||||
// LEFT
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y + depth_interval, depth, height, interval, false, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX((width * length + thick) / 2.0);
|
||||
b.setTranslateX(-(width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
|
||||
// RIGHT
|
||||
getChildren().add(new Face(skin, start_x, start_y + depth_interval, depth, height, interval, true, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX(-(width * length + thick) / 2.0);
|
||||
b.setTranslateX((width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
|
||||
@@ -246,7 +246,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
} else {
|
||||
handler.resolve();
|
||||
}
|
||||
});
|
||||
}, "", new RequiredValidator(i18n("input.not_empty")));
|
||||
});
|
||||
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.game.GraphicsAPI;
|
||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
||||
import org.jackhuang.hmcl.game.Renderer;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
@@ -69,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
|
||||
private final LineSelectButton<Renderer> rendererPane;
|
||||
|
||||
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
|
||||
@@ -79,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
|
||||
));
|
||||
|
||||
@Nullable GameVersionNumber gameVersion = versionId != null
|
||||
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
|
||||
: null;
|
||||
|
||||
this.getStyleClass().add("gray-background");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
@@ -176,23 +183,55 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
nativesDirSublist.setHasSubtitle(true);
|
||||
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.advanced.natives_directory.custom"), NativesDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
|
||||
nativesDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
|
||||
nativesDirCustomOption
|
||||
));
|
||||
HintPane nativesDirHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
|
||||
nativesDirItem.getChildren().add(nativesDirHint);
|
||||
|
||||
graphicsBackendPane = new LineSelectButton<>();
|
||||
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
|
||||
graphicsBackendPane.setNullSafeConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
|
||||
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
|
||||
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
|
||||
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
|
||||
case VULKAN -> {
|
||||
if (gameVersion == null)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
|
||||
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
|
||||
else
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
|
||||
}
|
||||
default -> null;
|
||||
});
|
||||
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
|
||||
graphicsBackendPane.setItems(GraphicsAPI.values());
|
||||
|
||||
rendererPane = new LineSelectButton<>();
|
||||
rendererPane.setTitle(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
|
||||
rendererPane.setNullSafeConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
|
||||
rendererPane.setDescriptionConverter(e -> {
|
||||
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
});
|
||||
rendererPane.setItems(Renderer.values());
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
|
||||
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
|
||||
if (backend == null) { // unbind
|
||||
return;
|
||||
}
|
||||
|
||||
rendererPane.setItems(Renderer.getSupported(backend));
|
||||
if (backend == GraphicsAPI.DEFAULT) {
|
||||
rendererPane.setDisable(true);
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
} else {
|
||||
rendererPane.setDisable(false);
|
||||
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
}
|
||||
});
|
||||
|
||||
noJVMArgsPane = new LineToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
@@ -219,7 +258,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
noJVMCheckPane, noNativesPatchPane
|
||||
);
|
||||
|
||||
@@ -252,6 +291,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
@@ -294,6 +334,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -97,6 +98,14 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
listView = new JFXListView<>();
|
||||
filteredList = new FilteredList<>(skinnable.getItems());
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAllButton.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
searchBar = new HBox();
|
||||
@@ -123,12 +132,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
enableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
disableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
|
||||
selectingToolbar.getChildren().addAll(
|
||||
removeButton,
|
||||
enableButton,
|
||||
disableButton,
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
selectAllButton,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
@@ -184,6 +194,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
@@ -531,7 +531,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
private static final Insets PADDING = new Insets(9, 9, 0, 9);
|
||||
|
||||
@@ -578,7 +577,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
} else {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.getTitle());
|
||||
content.setSubtitle(item.getDescription());
|
||||
String description = item.getDescription();
|
||||
if (description != null) {
|
||||
description = description.replaceAll("\\R", " ");
|
||||
}
|
||||
content.setSubtitle(description);
|
||||
content.getTags().clear();
|
||||
for (String category : item.getCategories()) {
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
|
||||
@@ -225,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getSubtitleLabel().setWrapText(true);
|
||||
getSkinnable().addon.getCategories().stream()
|
||||
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
content.getFirstLine().setMinWidth(0);
|
||||
descriptionPane.getChildren().add(content);
|
||||
|
||||
if (getSkinnable().mod != null) {
|
||||
@@ -273,7 +275,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
|
||||
String gameVersion = repository.getGameVersion(game).orElse(null);
|
||||
|
||||
if (gameVersion != null) {
|
||||
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
|
||||
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
|
||||
if (modVersions != null && !modVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
|
||||
@@ -351,7 +353,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DependencyModItem extends StackPane {
|
||||
private static final class DependencyModItem extends LineButton {
|
||||
public static final EnumMap<RemoteMod.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
|
||||
Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"),
|
||||
Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"),
|
||||
@@ -367,16 +369,16 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
pane.setPadding(new Insets(0, 8, 0, 8));
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
pane.setMouseTransparent(true);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
var imageView = new ImageContainer(40);
|
||||
pane.getChildren().setAll(imageView, content);
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
FXUtils.onClicked(container, () -> {
|
||||
FXUtils.setLimitHeight(this, 60);
|
||||
setOnAction((e) -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(new DownloadPage(page, addon, version, callback));
|
||||
});
|
||||
getChildren().setAll(container);
|
||||
setNode(IDX_LEADING, pane);
|
||||
|
||||
if (addon != RemoteMod.BROKEN) {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
@@ -439,7 +441,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
case NEO_FORGE:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
@@ -493,6 +495,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
|
||||
+19
-6
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -62,17 +63,29 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
|
||||
supportChinese.set(true);
|
||||
|
||||
boolean supportedCurseForge = CurseForgeRemoteModRepository.isAvailable() && curseForge != null;
|
||||
|
||||
downloadSources.setAll("mods.modrinth");
|
||||
if (CurseForgeRemoteModRepository.isAvailable()) {
|
||||
if (supportedCurseForge) {
|
||||
downloadSources.add("mods.curseforge");
|
||||
}
|
||||
|
||||
if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else if (curseForge != null) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
if ("curseforge".equalsIgnoreCase(config().getDefaultAddonSource())) {
|
||||
if (supportedCurseForge) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
} else if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else if (supportedCurseForge) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,11 +160,11 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGED);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,10 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -154,6 +157,18 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
);
|
||||
|
||||
// Toolbar Selecting
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAll.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
@@ -171,8 +186,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectAll()),
|
||||
selectAll,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
@@ -473,7 +487,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGED -> i18n("install.installer.neoforge");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
@@ -643,7 +657,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGED -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
|
||||
@@ -23,7 +23,10 @@ import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -157,20 +160,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return;
|
||||
|
||||
Path parent = currentDirectory.path;
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
Controllers.prompt(
|
||||
i18n("schematics.create_directory.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = parent.resolve(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||
@@ -185,7 +177,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||
}
|
||||
}));
|
||||
}, "", new RequiredValidator(), new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid));
|
||||
}
|
||||
|
||||
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
|
||||
|
||||
@@ -30,8 +30,10 @@ import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
@@ -39,8 +41,10 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -186,15 +190,34 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private void clearLibraries() {
|
||||
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
|
||||
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
|
||||
Task.runAsync(Schedulers.io(), () -> {
|
||||
FileUtils.deleteDirectoryQuietly(libraries);
|
||||
}).whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void clearAssets() {
|
||||
HMCLGameRepository baseDirectory = getProfile().getRepository();
|
||||
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
|
||||
if (version.get() != null) {
|
||||
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
|
||||
}
|
||||
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
|
||||
|
||||
Profile.ProfileVersion currentVersion = version.get();
|
||||
Path resourcesDir = currentVersion != null
|
||||
? getProfile().getRepository().getRunDirectory(currentVersion.getVersion()).resolve("resources")
|
||||
: null;
|
||||
|
||||
Task.runAsync(Schedulers.io(), () -> {
|
||||
FileUtils.deleteDirectoryQuietly(assetsDir);
|
||||
if (resourcesDir != null) {
|
||||
FileUtils.deleteDirectoryQuietly(resourcesDir);
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
|
||||
@@ -239,7 +239,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
|
||||
|
||||
gameDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
|
||||
@@ -336,7 +336,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
launcherVisibilityPane = new LineSelectButton<>();
|
||||
launcherVisibilityPane.setTitle(i18n("settings.advanced.launcher_visible"));
|
||||
launcherVisibilityPane.setItems(LauncherVisibility.values());
|
||||
launcherVisibilityPane.setConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
|
||||
launcherVisibilityPane.setNullSafeConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
|
||||
|
||||
BorderPane dimensionPane = new BorderPane();
|
||||
{
|
||||
@@ -351,7 +351,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
cboWindowsSize.setPrefWidth(150);
|
||||
right.setLeft(cboWindowsSize);
|
||||
cboWindowsSize.setEditable(true);
|
||||
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
|
||||
cboWindowsSize.setPromptText("854x480");
|
||||
cboWindowsSize.getItems().setAll(getSupportedResolutions());
|
||||
|
||||
@@ -373,7 +372,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
|
||||
processPriorityPane = new LineSelectButton<>();
|
||||
processPriorityPane.setTitle(i18n("settings.advanced.process_priority"));
|
||||
processPriorityPane.setConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
|
||||
processPriorityPane.setNullSafeConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
|
||||
processPriorityPane.setDescriptionConverter(e -> {
|
||||
String bundleKey = "settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
|
||||
@@ -22,8 +22,11 @@ import javafx.application.Platform;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
@@ -146,7 +149,8 @@ public final class Versions {
|
||||
} else {
|
||||
handler.reject(i18n("version.manage.rename.fail"));
|
||||
}
|
||||
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
}, version,
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||
}
|
||||
|
||||
@@ -158,6 +162,43 @@ public final class Versions {
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
|
||||
}
|
||||
|
||||
public static void installFromJson(Profile profile, Path file) {
|
||||
Version version;
|
||||
try {
|
||||
version = profile.getRepository().readVersionJson(file);
|
||||
} catch (Exception e) {
|
||||
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
|
||||
handler.resolve();
|
||||
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
Version newVersion = version.setId(result).setJar(result);
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
|
||||
Task.allOf(
|
||||
new GameAssetDownloadTask(dependencyManager, newVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
|
||||
new GameLibrariesTask(dependencyManager, newVersion, true)
|
||||
).withRunAsync(() -> {
|
||||
// ignore failure
|
||||
}))
|
||||
.thenComposeAsync(repository.saveAsync(newVersion))
|
||||
.thenRunAsync(repository::refreshVersions)
|
||||
.whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception == null) {
|
||||
profile.setSelectedVersion(result);
|
||||
} else {
|
||||
Controllers.dialog(
|
||||
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)));
|
||||
}
|
||||
|
||||
public static void duplicateVersion(Profile profile, String version) {
|
||||
Controllers.prompt(
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||
@@ -223,6 +264,11 @@ public final class Versions {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
|
||||
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
|
||||
String defaultExt = getDefaultScriptExtension();
|
||||
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
|
||||
}
|
||||
|
||||
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
@@ -232,6 +278,21 @@ public final class Versions {
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isValidScriptExtension(String ext) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return ext.equalsIgnoreCase("bat") || ext.equalsIgnoreCase("ps1");
|
||||
}
|
||||
return ext.equalsIgnoreCase("sh") || ext.equalsIgnoreCase("bash") || ext.equalsIgnoreCase("command") || ext.equalsIgnoreCase("ps1");
|
||||
}
|
||||
|
||||
private static String getDefaultScriptExtension() {
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> "bat";
|
||||
case MACOS -> "command";
|
||||
default -> "sh";
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(profile, id))
|
||||
|
||||
@@ -40,10 +40,12 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
@@ -153,7 +155,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
LOG.warning("Failed to create backup", exception);
|
||||
Controllers.dialog(i18n("world.backup.create.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
}), i18n("world.backup"), null);
|
||||
}), i18n("world.backup"), TaskCancellationAction.NO_CANCEL);
|
||||
}
|
||||
|
||||
private final class WorldBackupsPageSkin extends ToolbarListPageSkin<BackupInfo, WorldBackupsPage> {
|
||||
|
||||
@@ -24,13 +24,11 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.effect.BoxBlur;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.nbt.tag.*;
|
||||
@@ -162,11 +160,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
seedPane.setTitle(i18n("world.info.random_seed"));
|
||||
|
||||
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||
StackPane visibilityButton = new StackPane();
|
||||
JFXButton visibilityButton = FXUtils.newToggleButton4(SVG.VISIBILITY_OFF);
|
||||
{
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
visibilityButton.setAlignment(Pos.CENTER);
|
||||
visibilityButton.setOnAction((event) -> visibility.set(!visibility.get()));
|
||||
}
|
||||
|
||||
Label seedLabel = new Label();
|
||||
@@ -180,7 +177,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
blur.setIterations(3);
|
||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||
visibilityButton.setGraphic(icon.createIcon(16));
|
||||
seedLabel.setEffect(isVisibility ? null : blur);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ import org.jackhuang.hmcl.game.WorldLockedException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
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.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -89,20 +90,9 @@ public final class WorldManageUIUtils {
|
||||
|
||||
public static void copyWorld(World world, Runnable runnable) {
|
||||
Path worldPath = world.getFile();
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
Controllers.prompt(
|
||||
i18n("world.duplicate.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = worldPath.resolveSibling(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("world.duplicate.failed.already_exists"));
|
||||
@@ -125,7 +115,7 @@ public final class WorldManageUIUtils {
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}));
|
||||
}, "", new RequiredValidator(), new Validator(i18n("world.duplicate.failed.invalid_name"), FileUtils::isNameValid));
|
||||
}
|
||||
|
||||
public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException {
|
||||
|
||||
@@ -27,8 +27,8 @@ final class HMCLDownloadTask extends FileDownloadTask {
|
||||
private final RemoteVersion.Type archiveFormat;
|
||||
|
||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||
super(version.getUrl(), target, version.getIntegrityCheck());
|
||||
archiveFormat = version.getType();
|
||||
super(version.url(), target, version.integrityCheck());
|
||||
archiveFormat = version.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,11 +23,13 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class RemoteVersion {
|
||||
public record RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck,
|
||||
boolean preview, boolean force) {
|
||||
|
||||
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
|
||||
try {
|
||||
@@ -46,54 +48,8 @@ public final class RemoteVersion {
|
||||
}
|
||||
}
|
||||
|
||||
private final UpdateChannel channel;
|
||||
private final String version;
|
||||
private final String url;
|
||||
private final Type type;
|
||||
private final IntegrityCheck integrityCheck;
|
||||
private final boolean preview;
|
||||
private final boolean force;
|
||||
|
||||
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
|
||||
this.channel = channel;
|
||||
this.version = version;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.integrityCheck = integrityCheck;
|
||||
this.preview = preview;
|
||||
this.force = force;
|
||||
}
|
||||
|
||||
public UpdateChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public IntegrityCheck getIntegrityCheck() {
|
||||
return integrityCheck;
|
||||
}
|
||||
|
||||
public boolean isPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public boolean isForce() {
|
||||
return force;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "[" + version + " from " + url + "]";
|
||||
}
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ public final class UpdateChecker {
|
||||
RemoteVersion latest = latestVersion.get();
|
||||
if (latest == null || isDevelopmentVersion(Metadata.VERSION)) {
|
||||
return false;
|
||||
} else if (latest.isForce()
|
||||
} else if (latest.force()
|
||||
|| Metadata.isNightly()
|
||||
|| latest.getChannel() == UpdateChannel.NIGHTLY
|
||||
|| latest.getChannel() != UpdateChannel.getChannel()) {
|
||||
return !latest.getVersion().equals(Metadata.VERSION);
|
||||
|| latest.channel() == UpdateChannel.NIGHTLY
|
||||
|| latest.channel() != UpdateChannel.getChannel()) {
|
||||
return !latest.version().equals(Metadata.VERSION);
|
||||
} else {
|
||||
return VersionNumber.compare(Metadata.VERSION, latest.getVersion()) < 0;
|
||||
return VersionNumber.compare(Metadata.VERSION, latest.version()) < 0;
|
||||
}
|
||||
},
|
||||
latestVersion);
|
||||
|
||||
@@ -20,20 +20,20 @@ package org.jackhuang.hmcl.upgrade;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import org.jackhuang.hmcl.EntryPoint;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.UpgradeDialog;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -43,13 +43,15 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
|
||||
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;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class UpdateHandler {
|
||||
private UpdateHandler() {
|
||||
@@ -67,7 +69,7 @@ public final class UpdateHandler {
|
||||
performMigration();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to perform migration", e);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -82,7 +84,7 @@ public final class UpdateHandler {
|
||||
applyUpdate(Paths.get(args[1]));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to apply update", e);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -125,6 +127,29 @@ public final class UpdateHandler {
|
||||
throw new IOException("Current JAR is not verified");
|
||||
}
|
||||
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
Controllers.saveWindowStates();
|
||||
} finally {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException | InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
requestUpdate(downloaded, getCurrentLocation());
|
||||
EntryPoint.exit(0);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -189,15 +189,16 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
if (renderer == Renderer.DEFAULT)
|
||||
return null;
|
||||
|
||||
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
return getNatives(java.getPlatform()).get("mesa-loader");
|
||||
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
|
||||
if (renderer == Renderer.LLVMPIPE)
|
||||
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
|
||||
if (renderer == Renderer.OpenGL.LLVMPIPE)
|
||||
return getNatives(java.getPlatform()).get("software-renderer-loader");
|
||||
else
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
// Copy from javafx.swing
|
||||
/*
|
||||
* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.awt.image.SampleModel;
|
||||
import java.awt.image.SinglePixelPackedSampleModel;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for converting data types between
|
||||
* Swing/AWT and JavaFX formats.
|
||||
*
|
||||
* @since JavaFX 2.2
|
||||
*/
|
||||
public final class SwingFXUtils {
|
||||
private SwingFXUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshots the specified {@link BufferedImage} and stores a copy of
|
||||
* its pixels into a JavaFX {@link Image} object, creating a new
|
||||
* object if needed.
|
||||
* The returned {@code Image} will be a static snapshot of the state
|
||||
* of the pixels in the {@code BufferedImage} at the time the method
|
||||
* completes. Further changes to the {@code BufferedImage} will not
|
||||
* be reflected in the {@code Image}.
|
||||
* <p>
|
||||
* The optional JavaFX {@link WritableImage} parameter may be reused
|
||||
* to store the copy of the pixels.
|
||||
* A new {@code Image} will be created if the supplied object is null,
|
||||
* is too small or of a type which the image pixels cannot be easily
|
||||
* converted into.
|
||||
*
|
||||
* @param bimg the {@code BufferedImage} object to be converted
|
||||
* @param wimg an optional {@code WritableImage} object that can be
|
||||
* used to store the returned pixel data
|
||||
* @return an {@code Image} object representing a snapshot of the
|
||||
* current pixels in the {@code BufferedImage}.
|
||||
* @since JavaFX 2.2
|
||||
*/
|
||||
public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) {
|
||||
int bw = bimg.getWidth();
|
||||
int bh = bimg.getHeight();
|
||||
switch (bimg.getType()) {
|
||||
case BufferedImage.TYPE_INT_ARGB:
|
||||
case BufferedImage.TYPE_INT_ARGB_PRE:
|
||||
break;
|
||||
default:
|
||||
BufferedImage converted =
|
||||
new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE);
|
||||
Graphics2D g2d = converted.createGraphics();
|
||||
g2d.drawImage(bimg, 0, 0, null);
|
||||
g2d.dispose();
|
||||
bimg = converted;
|
||||
break;
|
||||
}
|
||||
// assert(bimg.getType == TYPE_INT_ARGB[_PRE]);
|
||||
if (wimg != null) {
|
||||
int iw = (int) wimg.getWidth();
|
||||
int ih = (int) wimg.getHeight();
|
||||
if (iw < bw || ih < bh) {
|
||||
wimg = null;
|
||||
} else if (bw < iw || bh < ih) {
|
||||
int[] empty = new int[iw];
|
||||
PixelWriter pw = wimg.getPixelWriter();
|
||||
PixelFormat<IntBuffer> pf = PixelFormat.getIntArgbPreInstance();
|
||||
if (bw < iw) {
|
||||
pw.setPixels(bw, 0, iw - bw, bh, pf, empty, 0, 0);
|
||||
}
|
||||
if (bh < ih) {
|
||||
pw.setPixels(0, bh, iw, ih - bh, pf, empty, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wimg == null) {
|
||||
wimg = new WritableImage(bw, bh);
|
||||
}
|
||||
PixelWriter pw = wimg.getPixelWriter();
|
||||
DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer();
|
||||
int[] data = db.getData();
|
||||
int offset = bimg.getRaster().getDataBuffer().getOffset();
|
||||
int scan = 0;
|
||||
SampleModel sm = bimg.getRaster().getSampleModel();
|
||||
if (sm instanceof SinglePixelPackedSampleModel) {
|
||||
scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride();
|
||||
}
|
||||
|
||||
PixelFormat<IntBuffer> pf = (bimg.isAlphaPremultiplied() ?
|
||||
PixelFormat.getIntArgbPreInstance() :
|
||||
PixelFormat.getIntArgbInstance());
|
||||
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
|
||||
return wimg;
|
||||
}
|
||||
|
||||
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return toFXImage(bimg, null);
|
||||
}
|
||||
|
||||
int width = (int) requestedWidth;
|
||||
int height = (int) requestedHeight;
|
||||
|
||||
// Calculate actual dimensions if preserveRatio is true
|
||||
if (preserveRatio) {
|
||||
double originalWidth = bimg.getWidth();
|
||||
double originalHeight = bimg.getHeight();
|
||||
double scaleX = requestedWidth / originalWidth;
|
||||
double scaleY = requestedHeight / originalHeight;
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
width = (int) (originalWidth * scale);
|
||||
height = (int) (originalHeight * scale);
|
||||
}
|
||||
|
||||
// Create scaled BufferedImage
|
||||
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
|
||||
Graphics2D g2d = scaledImage.createGraphics();
|
||||
try {
|
||||
if (smooth) {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
}
|
||||
|
||||
g2d.drawImage(bimg, 0, 0, width, height, null);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
return toFXImage(scaledImage, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
|
||||
case GA -> {
|
||||
// do nothing
|
||||
}
|
||||
case SNAPSHOT -> {
|
||||
builder.append("之快照");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
}
|
||||
case PRE_RELEASE -> {
|
||||
builder.append("之預");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
@@ -155,7 +159,32 @@ public class Translator_lzh extends Translator {
|
||||
case "23w13a_or_b" -> "二三週一三暨";
|
||||
case "24w14potato" -> "二四週一四芋";
|
||||
case "25w14craftmine" -> "二五週一四礦";
|
||||
default -> version;
|
||||
default -> {
|
||||
if (version.length() >= 6 && version.charAt(2) == 'w') {
|
||||
Matcher matcher = Pattern.compile("(?<year>[0-9]{2})w(?<week>[0-9]{2})(?<suffix>.+)").matcher(version);
|
||||
if (matcher.matches()) {
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int week = Integer.parseInt(matcher.group("week"));
|
||||
String suffix = matcher.group("suffix");
|
||||
char suffixFirstChar = suffix.charAt(0);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendDigitByDigit(builder, String.valueOf(year));
|
||||
builder.append('週');
|
||||
appendDigitByDigit(builder, String.valueOf(week));
|
||||
|
||||
if (suffix.length() == 1
|
||||
&& suffixFirstChar >= 'a' && (suffixFirstChar - 'a') < TIAN_GAN.length)
|
||||
builder.append(TIAN_GAN[suffixFirstChar - 'a']);
|
||||
else
|
||||
builder.append(suffix);
|
||||
|
||||
yield builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
yield version;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return gameVersion.toString();
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{
|
||||
"title" : "HelloNBT",
|
||||
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/Glavo/HelloNBT"
|
||||
"externalLink" : "https://github.com/HMCL-dev/HelloNBT"
|
||||
},
|
||||
{
|
||||
"title" : "minecraft-jfx-skin",
|
||||
@@ -69,6 +69,11 @@
|
||||
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/aellerton/japng"
|
||||
},
|
||||
{
|
||||
"title" : "fxsvgimage",
|
||||
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
|
||||
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
|
||||
},
|
||||
{
|
||||
"title": "Terracotta",
|
||||
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",
|
||||
@@ -83,5 +88,20 @@
|
||||
"title": "MonetFX",
|
||||
"subtitle": "Copyright © 2025 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/Glavo/MonetFX"
|
||||
},
|
||||
{
|
||||
"title": "LWJGL Unsafe Agent",
|
||||
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
|
||||
},
|
||||
{
|
||||
"title": "TomlJ",
|
||||
"subtitle": "Licensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/tomlj/tomlj"
|
||||
},
|
||||
{
|
||||
"title": "WebURL for Java",
|
||||
"subtitle": "Licensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/Glavo/weburl-java"
|
||||
}
|
||||
]
|
||||
@@ -70,6 +70,12 @@
|
||||
"subtitleLocalized" : "about.thanks_to.mcim.statement",
|
||||
"externalLink" : "https://github.com/mcmod-info-mirror"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/8mi-tech.png",
|
||||
"titleLocalized" : "about.thanks_to.8mi-tech",
|
||||
"subtitleLocalized" : "about.thanks_to.8mi-tech.statement",
|
||||
"externalLink" : "https://8mi.tech"
|
||||
},
|
||||
{
|
||||
"image" : {
|
||||
"light" : "/assets/img/github.png",
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
.scroll-bar .thumb {
|
||||
-fx-fill: -monet-surface-tint;
|
||||
-fx-arc-width: 8;
|
||||
-fx-arc-height: 8;
|
||||
-fx-arc-width: 6;
|
||||
-fx-arc-height: 6;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
@@ -414,7 +414,7 @@
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
|
||||
-fx-pref-width: 80px;
|
||||
-fx-pref-width: 100px;
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
|
||||
@@ -1093,8 +1093,12 @@
|
||||
-fx-background-color: -monet-error-container;
|
||||
}
|
||||
|
||||
.multi-file-item {
|
||||
-fx-padding: 10 0 10 0;
|
||||
}
|
||||
|
||||
.options-sublist {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-list-item {
|
||||
@@ -1105,6 +1109,14 @@
|
||||
-fx-font-size: 12;
|
||||
}
|
||||
|
||||
.options-sublist .options-list-item {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-sublist .multi-file-item {
|
||||
-fx-padding: 10 10 10 10;
|
||||
}
|
||||
|
||||
.options-list-item .svg {
|
||||
-fx-fill: -monet-on-surface;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
@@ -1140,6 +1152,10 @@
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper > .container {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
@@ -1463,6 +1479,11 @@
|
||||
-jfx-radius: 9;
|
||||
}
|
||||
|
||||
.spinner-pane .spinner-pane-content {
|
||||
-fx-padding: 10px;
|
||||
-fx-spacing: 10px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Combo Box *
|
||||
@@ -1477,6 +1498,11 @@
|
||||
-fx-max-width: 1000000000;
|
||||
}
|
||||
|
||||
.jfx-combo-box .arrow {
|
||||
-fx-background-color: -monet-on-surface-variant;
|
||||
-fx-shape: "M12 15 7 10H17L12 15Z";
|
||||
}
|
||||
|
||||
.jfx-combo-box .text {
|
||||
-fx-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -1492,6 +1518,7 @@
|
||||
.combo-box-popup .list-view .jfx-list-cell {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-cursor: hand;
|
||||
-fx-background-insets: 0.0;
|
||||
}
|
||||
|
||||
@@ -1501,6 +1528,10 @@
|
||||
-fx-text-fill: -monet-on-secondary-container;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .list-cell:hover > .jfx-rippler > StackPane {
|
||||
-fx-background-color: -monet-secondary-container-transparent-50;
|
||||
}
|
||||
|
||||
.jfx-combo-box-warning {
|
||||
-jfx-focus-color: #D34336;
|
||||
-jfx-unfocus-color: #D34336;
|
||||
@@ -1565,6 +1596,21 @@
|
||||
-fx-background-color: -monet-on-surface;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-actions {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-padding: 0.0 10.0 5.0 0.0;
|
||||
-fx-alignment: center-right;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-accept {
|
||||
-fx-padding: 0.7em 0.8em;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-cancel {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
-fx-padding: 0.7em 0.8em;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Table View *
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -48,6 +48,8 @@ about.thanks_to.shulkersakura.statement=For providing the logo for HMCL.
|
||||
about.thanks_to.users=HMCL User Group Members
|
||||
about.thanks_to.users.statement=Thanks for donations, bug reports, and so on.
|
||||
about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support.
|
||||
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
|
||||
about.thanks_to.8mi-tech.statement=Provides an accelerated download service for some of the game's static resources.
|
||||
about.open_source=Open Source
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
@@ -108,9 +110,9 @@ account.methods.microsoft.close_page=Microsoft account authorization is now comp
|
||||
account.methods.microsoft.deauthorize=Deauthorize
|
||||
account.methods.microsoft.error.add_family=Please click <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">here</a> to change your account birth date to be over 18 years old, or add your account to a family.
|
||||
account.methods.microsoft.error.country_unavailable=Xbox Live is not available in your current country/region.
|
||||
account.methods.microsoft.error.missing_xbox_account=Your Microsoft account does not have a linked Xbox account yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to link one.
|
||||
account.methods.microsoft.error.no_character=Please confirm that you have purchased Minecraft: Java Edition.\n\
|
||||
If you have already purchased it, a game profile may not have been created. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create a game profile.
|
||||
account.methods.microsoft.error.missing_xbox_account=Please confirm that you have purchased Minecraft: Java Edition. If you have, please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create your game profile.
|
||||
account.methods.microsoft.error.no_license=No Minecraft purchase record found. Please click <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">here</a> to get the game.\nIf you are using Game Pass, please create your game profile <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a>. If you have already purchased the game, please check if you are logged into the correct account.
|
||||
account.methods.microsoft.error.no_character=You have not created a game profile yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create one.
|
||||
account.methods.microsoft.error.banned=Your account may have been banned by Xbox Live.\n\
|
||||
You can click <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">here</a> to check the ban status of your account.
|
||||
account.methods.microsoft.error.unknown=Failed to log in, error code: %d.
|
||||
@@ -363,16 +365,16 @@ download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
download.failed.refresh=Failed to fetch version list. Please click here to retry.
|
||||
download.game=New Game
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland.
|
||||
download.provider.mojang=Official
|
||||
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
|
||||
download.provider.mojang.desc=Note that OptiFine is provided by BMCLAPI.
|
||||
download.provider.official=From Official Sources
|
||||
download.provider.official.desc=Latest, but may load slowly
|
||||
download.provider.official.desc=Always obtain from official sources.
|
||||
download.provider.balanced=From Fastest Available
|
||||
download.provider.balanced.desc=Balanced, but may not be the latest
|
||||
download.provider.balanced.desc=Best choice for ALL users.
|
||||
download.provider.mirror=From Mirror
|
||||
download.provider.mirror.desc=Fast, but may not be the latest
|
||||
download.provider.mirror.desc=Recommended option for users in Chinese Mainland.
|
||||
download.java=Downloading Java
|
||||
download.java.process=Java Download Process
|
||||
download.javafx=Downloading dependencies for the launcher...
|
||||
@@ -452,12 +454,12 @@ fatal.unsupported_platform=Minecraft is not fully supported on your platform yet
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
|
||||
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
|
||||
If you encounter problems when playing a game, launching the game with Java based on x86-64 architecture may offer better compatibility.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
\n\
|
||||
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
|
||||
Click the link to navigate to the Microsoft Store and install the compatibility pack.
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.
|
||||
|
||||
file=File
|
||||
|
||||
@@ -760,6 +762,7 @@ install.new_game.already_exists=This instance name already exists. Please use an
|
||||
install.new_game.current_game_version=Current Instance Version
|
||||
install.new_game.installation=Instance Installation
|
||||
install.new_game.malformed=Invalid name.
|
||||
install.new_game.malformed_json=Invalid Minecraft instance JSON file.
|
||||
install.select=Choose operation
|
||||
install.success=Successfully installed.
|
||||
|
||||
@@ -1229,7 +1232,7 @@ world.load.fail=Failed to load world
|
||||
world.locked=In use
|
||||
world.locked.failed=The world is currently in use. Please close the game and try again.
|
||||
world.manage=Worlds
|
||||
world.manage.button=World Management
|
||||
world.manage.button=Manage World
|
||||
world.manage.title=World - %s
|
||||
world.name=World Name
|
||||
world.name.enter=Enter the world name
|
||||
@@ -1306,6 +1309,7 @@ search.page_n=%1$d / %2$s
|
||||
|
||||
selector.choose=Choose
|
||||
selector.choose_file=Choose file
|
||||
selector.choose_directory=Choose directory
|
||||
selector.custom=Custom
|
||||
|
||||
settings=Settings
|
||||
@@ -1334,6 +1338,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
|
||||
settings.advanced.environment_variables=Environment Variables
|
||||
settings.advanced.game_dir.default=Default (".minecraft/")
|
||||
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
|
||||
settings.advanced.graphics_backend=Graphics API
|
||||
settings.advanced.graphics_backend.default=Default
|
||||
settings.advanced.graphics_backend.default.desc=Follow the game's settings
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
|
||||
settings.advanced.java_permanent_generation_space=PermGen Space
|
||||
settings.advanced.java_permanent_generation_space.prompt=in MiB
|
||||
settings.advanced.jvm=JVM Options
|
||||
@@ -1353,10 +1366,6 @@ settings.advanced.natives_directory.choose=Choose the location of the desired na
|
||||
settings.advanced.natives_directory.custom=Custom
|
||||
settings.advanced.natives_directory.default=Default
|
||||
settings.advanced.natives_directory.default.version_id=<Instance Name>
|
||||
settings.advanced.natives_directory.hint=This option is intended only for users of Apple silicon or other not officially supported platforms. Please do not edit this option unless you know what you are doing.\n\
|
||||
\n\
|
||||
Before proceeding, please make sure all libraries (e.g. lwjgl.dll, libopenal.so) are provided in your desired directory.\n\
|
||||
Note: It is recommended to use a fully English-letters path for the specified local library file. Otherwise it may lead to game launch failure.
|
||||
settings.advanced.no_jvm_args=Do not add default JVM arguments
|
||||
settings.advanced.no_optimizing_jvm_args=Do not add default JVM optimization arguments
|
||||
settings.advanced.precall_command=Pre-launch Command
|
||||
@@ -1376,13 +1385,47 @@ settings.advanced.post_exit_command=Post-exit Command
|
||||
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
|
||||
settings.advanced.renderer=Renderer
|
||||
settings.advanced.renderer.default=Default
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
|
||||
settings.advanced.renderer.default.desc=Use System Default
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
|
||||
settings.advanced.renderer.qualcomm=Qualcomm
|
||||
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.turnip=Qualcomm (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
|
||||
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
|
||||
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
@@ -1439,7 +1482,9 @@ settings.launcher.download.threads.auto=Automatically Determine
|
||||
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.download_source.auto=Automatically Choose Download Sources
|
||||
settings.launcher.default_addon_source=Default Addon Source
|
||||
settings.launcher.enable_game_list=Show instance list in homepage
|
||||
settings.launcher.misc=Misc
|
||||
settings.launcher.font=Font
|
||||
settings.launcher.font.anti_aliasing=Anti-aliasing
|
||||
settings.launcher.font.anti_aliasing.auto=Auto
|
||||
@@ -1587,7 +1632,7 @@ update=Update
|
||||
update.accept=Update
|
||||
update.changelog=Changelog
|
||||
update.channel.dev=Beta
|
||||
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Release channel builds.\n\
|
||||
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
|
||||
\n\
|
||||
If you encounter any bugs or problems, please submit feedback via the channels provided on the <a href="hmcl://settings/feedback">Feedback</a> page.\n\
|
||||
\n\
|
||||
@@ -1600,7 +1645,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
|
||||
\n\
|
||||
Follow <a href="https://space.bilibili.com/1445341">@huanghongxun</a> on Bilibili to stay up to date on important HMCL news, or <a href="https://space.bilibili.com/20314891">@Glavo</a> to learn about HMCL development progress.
|
||||
update.channel.nightly.title=Nightly Channel Notice
|
||||
update.channel.stable=Release
|
||||
update.channel.stable=Stable
|
||||
update.checking=Checking for updates
|
||||
update.disable_auto_show_update_dialog=Do not show update dialog automatically
|
||||
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
|
||||
@@ -1610,7 +1655,7 @@ update.newest_version=Latest version: %s
|
||||
update.bubble.title=Update Available: %s
|
||||
update.bubble.subtitle=Click here to update
|
||||
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
|
||||
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes, but may also have more issues due to insufficient testing.
|
||||
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
|
||||
update.latest=This is the latest version
|
||||
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
|
||||
update.tooltip=Update
|
||||
|
||||
@@ -354,18 +354,11 @@ download.external_link=زيارة موقع التنزيل
|
||||
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
|
||||
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
|
||||
download.failed.no_code=فشل التنزيل
|
||||
download.failed.refresh=فشل جلب قائمة الإصدارات. انقر هنا لإعادة المحاولة.
|
||||
download.game=لعبة جديدة
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=رسمي
|
||||
download.provider.mojang.desc=يتم توفير OptiFine عبر BMCLAPI
|
||||
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
|
||||
download.game=New Game
|
||||
download.provider.official=من المصادر الرسمية
|
||||
download.provider.official.desc=الأحدث، لكن قد يكون التحميل بطيئاً
|
||||
download.provider.balanced=من أسرع المصادر المتاحة
|
||||
download.provider.balanced.desc=متوازن، لكن قد لا يكون الأحدث
|
||||
download.provider.mirror=من مرآة
|
||||
download.provider.mirror.desc=سريع، لكن قد لا يكون الأحدث
|
||||
download.java=جارٍ تنزيل Java
|
||||
download.java.process=عملية تنزيل Java
|
||||
download.javafx=جارٍ تنزيل مكتبات المشغّل...
|
||||
@@ -445,8 +438,6 @@ fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية ب
|
||||
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
|
||||
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
|
||||
fatal.unsupported_platform.macos_arm64=وفّر Hello Minecraft! Launcher دعماً لمنصة Apple silicon، باستخدام Java ARM الأصلي لتشغيل الألعاب للحصول على تجربة لعب أكثر سلاسة.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، قد يوفر تشغيل اللعبة بـ Java المبني على معمارية x86-64 توافقاً أفضل.
|
||||
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
\n\
|
||||
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
|
||||
@@ -1218,7 +1209,6 @@ world.load.fail=فشل تحميل العالم
|
||||
world.locked=قيد الاستخدام
|
||||
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
|
||||
world.manage=العوالم
|
||||
world.manage.button=إدارة العوالم
|
||||
world.manage.title=العالم - %s
|
||||
world.name=اسم العالم
|
||||
world.name.enter=أدخل اسم العالم
|
||||
@@ -1342,10 +1332,6 @@ settings.advanced.natives_directory.choose=اختر موقع المكتبة ال
|
||||
settings.advanced.natives_directory.custom=مخصص
|
||||
settings.advanced.natives_directory.default=افتراضي
|
||||
settings.advanced.natives_directory.default.version_id=<اسم النسخة>
|
||||
settings.advanced.natives_directory.hint=هذا الخيار مخصص فقط لمستخدمي Apple silicon والمنصات غير المدعومة رسمياً. يُرجى عدم تعديل هذا الخيار إلا إذا كنت تعرف ما تفعله.\n\
|
||||
\n\
|
||||
قبل المتابعة، يُرجى التأكد من توفر جميع المكتبات (مثل lwjgl.dll وlibopenal.so) في المجلد المحدد.\n\
|
||||
ملاحظة: يُنصح باستخدام مسار يحتوي على حروف إنجليزية فقط لملف المكتبة المحلية المحدد، وإلا قد يتسبب ذلك في فشل تشغيل اللعبة.
|
||||
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
|
||||
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
|
||||
settings.advanced.precall_command=أمر ما قبل التشغيل
|
||||
@@ -1365,13 +1351,9 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
|
||||
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
|
||||
settings.advanced.renderer=المُصيِّر
|
||||
settings.advanced.renderer.default=افتراضي
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
|
||||
@@ -336,10 +336,6 @@ download.failed.empty=No hay versiones disponibles, por favor haga clic aquí pa
|
||||
download.failed.no_code=No se ha podido descargar
|
||||
download.failed.refresh=No se ha podido obtener la lista de versiones. Por favor, haga clic aquí para volver a intentarlo.
|
||||
download.game=Nuevo juego
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=Oficial
|
||||
download.provider.mojang.desc=OptiFine es proporcionado por BMCLAPI
|
||||
download.provider.official=De fuentes oficiales
|
||||
download.provider.balanced=De la fuente más rápida disponible
|
||||
download.provider.mirror=Desde espejo
|
||||
@@ -418,8 +414,6 @@ fatal.unsupported_platform=Minecraft aún no es totalmente compatible con tu pla
|
||||
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher ha prestado apoyo a la plataforma Loongson.\n\
|
||||
Si tienes problemas al jugar, puedes visitar https://docs.hmcl.net/groups.html para obtener ayuda.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher ha proporcionado soporte para la plataforma de chips de Apple, utilizando Java nativo de ARM para ejecutar juegos y conseguir una experiencia de juego más fluida.\n\
|
||||
Si tienes problemas al jugar a un juego, ejecutarlo con Java de arquitectura x86-64 puede tener mejor compatibilidad.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher ha proporcionado soporte nativo para la plataforma Windows en Arm. Si tiene problemas al jugar a un juego, intente iniciarlo con Java de arquitectura x86.\n\
|
||||
\n\
|
||||
Si utilizas la plataforma <b>Qualcomm</b>, es posible que tengas que instalar el <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">paquete de compatibilidad OpenGL</a> antes de jugar.\n\
|
||||
@@ -1138,7 +1132,6 @@ world.info.random_seed=Semilla
|
||||
world.locked=En uso
|
||||
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
|
||||
world.manage=Mundos
|
||||
world.manage.button=Administrar
|
||||
world.manage.title=Mundo - %s
|
||||
world.name=Nombre del mundo
|
||||
world.name.enter=Introducir el nombre del mundo
|
||||
@@ -1252,9 +1245,6 @@ settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca
|
||||
settings.advanced.natives_directory.custom=Personalizado
|
||||
settings.advanced.natives_directory.default=Por defecto
|
||||
settings.advanced.natives_directory.default.version_id=<Nombre de la instancia>
|
||||
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de chips de Apple u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
|
||||
\n\
|
||||
Antes de proceder, por favor, asegúrese de que todas las bibliotecas (por ejemplo, lwjgl.dll, libopenal.so) se proporcionan en su directorio deseado.
|
||||
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
|
||||
settings.advanced.no_optimizing_jvm_args=No añadir argumentos de optimización de JVM
|
||||
settings.advanced.precall_command=Comando pre-lanzamiento:
|
||||
@@ -1269,13 +1259,9 @@ settings.advanced.post_exit_command=Comando post-salida
|
||||
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
|
||||
settings.advanced.renderer=Renderizador
|
||||
settings.advanced.renderer.default=Por defecto
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
|
||||
settings.advanced.server_ip=Dirección del servidor
|
||||
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
|
||||
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
|
||||
|
||||
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user