Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8210c77de9 | ||
|
|
b11b298312 | ||
|
|
5c605c58d2 | ||
|
|
01d4100a2f | ||
|
|
d07eb2f748 | ||
|
|
345bb176fd | ||
|
|
83a38fadf0 | ||
|
|
9980dc8aea | ||
|
|
7dfbf76b69 | ||
|
|
7f2f77f54f |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
code_review:
|
||||
pull_request_opened:
|
||||
code_review: false
|
||||
@@ -1,105 +0,0 @@
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const ignoredFilePatterns = [
|
||||
/(?:^|\/)\.cnb\/.+\.ya?ml$/i,
|
||||
/(?:^|\/)\.gitee\/.+\.ya?ml$/i,
|
||||
/(?:^|\/)\.github\/.+\.ya?ml$/i,
|
||||
/(?:^|\/)\.idea\/.+\.xml$/i,
|
||||
/(?:^|\/)\.editorconfig$/i,
|
||||
/(?:^|\/)\.gitattributes$/i,
|
||||
/(?:^|\/)\.gitignore$/i,
|
||||
/(?:^|\/).+\.kts$/i,
|
||||
/(?:^|\/).+\.md$/i,
|
||||
/(?:^|\/)LICENSE(?:_.*)?$/i,
|
||||
/(?:^|\/)gradlew(?:\.bat|\.ps1)?$/i,
|
||||
/(?:^|\/)gradle\/wrapper\/gradle-wrapper\.properties$/i,
|
||||
/(?:^|\/)lang\/.+\.properties$/i,
|
||||
/(?:^|\/)l10n\/.+\.properties$/i,
|
||||
/(?:^|\/)checkstyle\.xml$/i,
|
||||
/(?:^|\/).+\.(?:png|jpe?g|gif|ico|svg|json|ttf|woff2?|webp|avif|jxl|heic|mp4|webm|mp3|ogg|wav|txt|toml)$/i
|
||||
];
|
||||
|
||||
async function processPR(issueNumber) {
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: issueNumber, per_page: 100,
|
||||
});
|
||||
|
||||
const countedFiles = [];
|
||||
const ignoredFiles = [];
|
||||
let totalChanges = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const ignored = ignoredFilePatterns.some(pattern => pattern.test(file.filename));
|
||||
if (ignored) {
|
||||
ignoredFiles.push(file.filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileChanges = file.changes ?? ((file.additions || 0) + (file.deletions || 0));
|
||||
totalChanges += fileChanges;
|
||||
countedFiles.push(`${file.filename} (${fileChanges})`);
|
||||
}
|
||||
|
||||
let targetLabel;
|
||||
if (totalChanges <= 9) targetLabel = "1+";
|
||||
else if (totalChanges <= 39) targetLabel = "10+";
|
||||
else if (totalChanges <= 99) targetLabel = "40+";
|
||||
else if (totalChanges <= 499) targetLabel = "100+";
|
||||
else if (totalChanges <= 999) targetLabel = "500+";
|
||||
else if (totalChanges <= 1999) targetLabel = "1000+";
|
||||
else if (totalChanges <= 4999) targetLabel = "2000+";
|
||||
else targetLabel = "5000+";
|
||||
|
||||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner, repo, issue_number: issueNumber, per_page: 100,
|
||||
});
|
||||
|
||||
const sizeLabels = currentLabels.map(label => label.name).filter(name => /^\d+\+$/.test(name));
|
||||
|
||||
for (const name of sizeLabels) {
|
||||
if (name !== targetLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: issueNumber, name,
|
||||
}).catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLabels.some(label => label.name === targetLabel)) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: issueNumber, labels: [targetLabel],
|
||||
});
|
||||
}
|
||||
|
||||
core.info(`Pull request #${issueNumber} has ${totalChanges} counted changed lines, applied ${targetLabel}.`);
|
||||
core.info(`Ignored files: ${ignoredFiles.length ? ignoredFiles.join(", ") : "none"}`);
|
||||
core.info(`Counted files: ${countedFiles.length ? countedFiles.join(", ") : "none"}`);
|
||||
}
|
||||
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
core.info("Manual trigger detected. Fetching all open pull requests...");
|
||||
|
||||
const openPRs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: "open", per_page: 100,
|
||||
});
|
||||
|
||||
const unlabelledPRs = openPRs.filter(pr => {
|
||||
return !pr.labels.some(label => /^\d+\+$/.test(label.name));
|
||||
});
|
||||
|
||||
core.info(`Found ${openPRs.length} open pull request(s), ${unlabelledPRs.length} of them lack a size label.`);
|
||||
|
||||
for (const pr of unlabelledPRs) {
|
||||
core.info(`----------------------------------------`);
|
||||
core.info(`Processing unlabelled pull request #${pr.number}: ${pr.title}`);
|
||||
await processPR(pr.number);
|
||||
}
|
||||
} else {
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.info("No pull request found in payload, skipping.");
|
||||
return;
|
||||
}
|
||||
await processPR(pr.number);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
name: PR Size Label
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
- name: Label PR by filtered changed lines
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require("./.github/scripts/pr-size-label.js");
|
||||
await script({ github, context, core });
|
||||
@@ -4,9 +4,9 @@ These rules apply to all Java code written or modified in this repository.
|
||||
|
||||
## Nullability
|
||||
|
||||
- Every class declared in a newly added Java source file must be annotated with JetBrains Annotations `@NotNullByDefault`.
|
||||
- When writing or modifying Java code, any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
|
||||
- Nullability in code being written or modified must never be implicit.
|
||||
- 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
|
||||
|
||||
@@ -15,8 +15,6 @@ These rules apply to all Java code written or modified in this repository.
|
||||
|
||||
## Documentation
|
||||
|
||||
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -52,7 +52,7 @@ if (buildNumber != null) {
|
||||
}
|
||||
}
|
||||
|
||||
val embedResources = configurations.register("embedResources")
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
@@ -166,7 +166,7 @@ val hmclProperties = buildList {
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile = tasks.register("createPropertiesFile") {
|
||||
val createPropertiesFile by tasks.registering {
|
||||
outputs.file(hmclPropertiesFile)
|
||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||
|
||||
@@ -258,7 +258,7 @@ tasks.processResources {
|
||||
|
||||
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
||||
|
||||
val makeExecutables = tasks.register("makeExecutables") {
|
||||
val makeExecutables by tasks.registering {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
dependsOn(tasks.jar)
|
||||
@@ -286,7 +286,7 @@ val makeExecutables = tasks.register("makeExecutables") {
|
||||
}
|
||||
}
|
||||
|
||||
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
|
||||
val makeDeb by tasks.registering(CreateDeb::class) {
|
||||
dependsOn(makeExecutables)
|
||||
|
||||
val debFile = layout.file(provider { artifactFile("deb") })
|
||||
@@ -438,19 +438,19 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
|
||||
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
||||
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
||||
}
|
||||
|
||||
val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList") {
|
||||
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
||||
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
||||
resourceBundleBaseName.set("I18N")
|
||||
additionalLanguages.set(listOf("en-Qabs"))
|
||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||
}
|
||||
|
||||
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
|
||||
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||
|
||||
@@ -52,6 +52,8 @@ public final class EntryPoint {
|
||||
|
||||
checkWine();
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
||||
if (!isInsideMacAppBundle())
|
||||
@@ -72,6 +74,71 @@ public final class EntryPoint {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
}
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createHMCLDirectories() {
|
||||
if (!Files.isDirectory(Metadata.HMCL_LOCAL_HOME)) {
|
||||
try {
|
||||
@@ -158,8 +225,15 @@ public final class EntryPoint {
|
||||
|
||||
private static void checkWine() {
|
||||
if (OperatingSystem.isRunningUnderWine()) {
|
||||
SwingUtils.initLookAndFeel();
|
||||
LOG.warning("HMCL is running under Wine or its distributions!");
|
||||
showWarning(i18n("fatal.wine_warning"));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,21 +273,10 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
static void showWarning(String message) {
|
||||
SwingUtils.initLookAndFeel();
|
||||
|
||||
int result = JOptionPane.showOptionDialog(null, message, i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
|
||||
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
static void showErrorAndExit(String message) {
|
||||
private static void showErrorAndExit(String message) {
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -30,6 +29,8 @@ 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;
|
||||
@@ -47,13 +48,7 @@ import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.User32;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
@@ -65,6 +60,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -102,7 +98,18 @@ public final class Launcher extends Application {
|
||||
}
|
||||
|
||||
try {
|
||||
initializeSettingsRuntime();
|
||||
try {
|
||||
SettingsManager.init();
|
||||
initializeSettingsRuntime();
|
||||
} catch (SambaException e) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
@@ -267,10 +274,52 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigOwner() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
return;
|
||||
|
||||
String userName = System.getProperty("user.name");
|
||||
Path configDirectory = SettingsManager.localConfigDirectory();
|
||||
if (!Files.exists(configDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String owner;
|
||||
try {
|
||||
owner = Files.getOwner(configDirectory).getName();
|
||||
} catch (IOException ioe) {
|
||||
LOG.warning("Failed to get file owner", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
|
||||
return;
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
files.add(configDirectory.toString());
|
||||
if (Files.exists(Metadata.HMCL_USER_HOME))
|
||||
files.add(Metadata.HMCL_USER_HOME.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
if (showAlert(AlertType.ERROR,
|
||||
i18n("fatal.config_loading_failure.unix", owner, command),
|
||||
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
Controllers.onApplicationStop();
|
||||
SettingsManager.shutdown();
|
||||
FileSaver.shutdown();
|
||||
LOG.shutdown();
|
||||
}
|
||||
|
||||
@@ -327,19 +376,6 @@ public final class Launcher extends Application {
|
||||
|
||||
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
||||
|
||||
try {
|
||||
SettingsManager.init();
|
||||
} catch (SambaException e) {
|
||||
EntryPoint.showWarning(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigOwner();
|
||||
SwingUtils.showErrorDialog(i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
@@ -372,158 +408,5 @@ public final class Launcher extends Application {
|
||||
});
|
||||
}
|
||||
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
}
|
||||
|
||||
setUpAnimationFrameRate:
|
||||
{
|
||||
if (System.getProperty("javafx.animation.pulse") != null) {
|
||||
break setUpAnimationFrameRate;
|
||||
}
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||
|
||||
try {
|
||||
int value = Integer.parseInt(animationFrameRate);
|
||||
if (value <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
if (value != 60)
|
||||
System.setProperty("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
break setUpAnimationFrameRate;
|
||||
}
|
||||
|
||||
// To avoid prematurely loading FXUtils, we only check if animationDisabled has been explicitly set to true
|
||||
if (Boolean.TRUE.equals(settings().animationDisabledProperty().get()))
|
||||
break setUpAnimationFrameRate;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (NativeUtils.USE_JNA && Gdi32.INSTANCE != null && User32.INSTANCE != null) {
|
||||
@Nullable Pointer pointer = User32.INSTANCE.GetDC(Pointer.NULL);
|
||||
if (pointer != null) {
|
||||
try {
|
||||
int refreshRate = Gdi32.INSTANCE.GetDeviceCaps(pointer, Gdi32.VREFRESH);
|
||||
|
||||
if (refreshRate > 0) {
|
||||
LOG.info("Detected refresh rate: " + refreshRate + "Hz");
|
||||
|
||||
if (refreshRate >= 90) {
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", String.valueOf(refreshRate));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
User32.INSTANCE.ReleaseDC(Pointer.NULL, pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigOwner() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
return;
|
||||
|
||||
String userName = System.getProperty("user.name");
|
||||
Path configDirectory = SettingsManager.localConfigDirectory();
|
||||
if (!Files.exists(configDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String owner;
|
||||
try {
|
||||
owner = Files.getOwner(configDirectory).getName();
|
||||
} catch (IOException ioe) {
|
||||
LOG.warning("Failed to get file owner", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
|
||||
return;
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
files.add(configDirectory.toString());
|
||||
if (Files.exists(Metadata.HMCL_USER_HOME))
|
||||
files.add(Metadata.HMCL_USER_HOME.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
SwingUtils.initLookAndFeel();
|
||||
|
||||
Object[] options = {i18n("button.copy_and_exit"), i18n("button.cancel")};
|
||||
int result = JOptionPane.showOptionDialog(null,
|
||||
i18n("fatal.config_loading_failure.unix", owner, command),
|
||||
i18n("message.error"),
|
||||
JOptionPane.DEFAULT_OPTION,
|
||||
JOptionPane.ERROR_MESSAGE,
|
||||
null,
|
||||
options,
|
||||
options[0]);
|
||||
|
||||
if (result == 0) {
|
||||
try {
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(command), null);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to copy command to clipboard", e);
|
||||
}
|
||||
}
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
||||
}
|
||||
@@ -52,7 +52,6 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -761,10 +760,7 @@ public final class LauncherHelper {
|
||||
case HIDE_AND_REOPEN:
|
||||
runLater(() -> {
|
||||
Optional.ofNullable(Controllers.getStage())
|
||||
.ifPresent(stage -> {
|
||||
stage.setIconified(false);
|
||||
stage.show();
|
||||
});
|
||||
.ifPresent(Stage::show);
|
||||
});
|
||||
break;
|
||||
case KEEP:
|
||||
@@ -899,10 +895,8 @@ public final class LauncherHelper {
|
||||
runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
@Nullable Stage stage = Controllers.getStage();
|
||||
if (stage != null) {
|
||||
stage.setIconified(false);
|
||||
stage.hide();
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().hide();
|
||||
launchingLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import kala.encdet.EncodingDetector;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -91,7 +91,7 @@ public final class LogExporter {
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
if (logMatcher == null || logMatcher.matches(file)) {
|
||||
try (BufferedReader reader = EncodingDetector.MODERN_WEB.newBufferedReader(file)) {
|
||||
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
||||
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read log file: " + file, e);
|
||||
|
||||
@@ -231,7 +231,7 @@ public final class ModpackHelper {
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
|
||||
/// Persistent configuration for a game directory.
|
||||
@JsonAdapter(GameDirectory.Serializer.class)
|
||||
@NotNullByDefault
|
||||
@@ -116,7 +118,7 @@ public final class GameDirectory implements Observable {
|
||||
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
|
||||
this.legacyGameSettings = new SimpleObjectProperty<>(this, "legacyGameSettings", legacyGameSettings);
|
||||
|
||||
addPropertyChangedListener(arg -> invalidate());
|
||||
addPropertyChangedListener(onInvalidating(this::invalidate));
|
||||
}
|
||||
|
||||
/// Returns a debug string containing the game directory path and display metadata.
|
||||
|
||||
@@ -156,14 +156,7 @@ final class JsonSettingFile<T extends ObservableSetting & JsonSchemaSetting> {
|
||||
///
|
||||
/// @param value the settings object to observe
|
||||
void installAutoSave(T value) {
|
||||
value.addListener(observable -> {
|
||||
if (value.shouldSaveImmediately(observable)) {
|
||||
save(value);
|
||||
value.setSavePending(false);
|
||||
} else {
|
||||
value.setSavePending(true);
|
||||
}
|
||||
});
|
||||
value.addListener(source -> save(value));
|
||||
}
|
||||
|
||||
/// Saves a settings object.
|
||||
|
||||
@@ -480,19 +480,17 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
|
||||
/// Whether UI animations are disabled.
|
||||
@SerializedName("animationDisabled")
|
||||
private final ObjectProperty<@Nullable Boolean> animationDisabled = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
|
||||
FXUtils.REDUCED_MOTION == Boolean.TRUE
|
||||
|| !JavaRuntime.CURRENT_JIT_ENABLED
|
||||
|| !FXUtils.GPU_ACCELERATION_ENABLED
|
||||
);
|
||||
|
||||
/// Returns the UI animation disable property.
|
||||
public ObjectProperty<@Nullable Boolean> animationDisabledProperty() {
|
||||
public BooleanProperty animationDisabledProperty() {
|
||||
return animationDisabled;
|
||||
}
|
||||
|
||||
public boolean isAnimationDisabled() {
|
||||
// Avoid accessing FXUtils too early during startup
|
||||
return Objects.requireNonNullElseGet(animationDisabled.get(), () ->
|
||||
FXUtils.REDUCED_MOTION == Boolean.TRUE || !JavaRuntime.CURRENT_JIT_ENABLED || !FXUtils.GPU_ACCELERATION_ENABLED);
|
||||
}
|
||||
|
||||
// Networks
|
||||
|
||||
/// Whether HMCL automatically selects the number of download threads.
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
@@ -55,15 +54,6 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
|
||||
register();
|
||||
}
|
||||
|
||||
/// Returns whether the changed field should trigger automatic persistence immediately.
|
||||
@Override
|
||||
public boolean shouldSaveImmediately(Observable observable) {
|
||||
return observable != x
|
||||
&& observable != y
|
||||
&& observable != width
|
||||
&& observable != height;
|
||||
}
|
||||
|
||||
/// The schema used by this launcher state file.
|
||||
@SerializedName(JsonSchema.PROPERTY_SCHEMA)
|
||||
private final ObjectProperty<JsonSchema> schema = new SimpleObjectProperty<>(CURRENT_SCHEMA);
|
||||
|
||||
@@ -55,20 +55,6 @@ public final class SettingsManager {
|
||||
private SettingsManager() {
|
||||
}
|
||||
|
||||
/// Saves deferred launcher state changes and shutdown file saver.
|
||||
public static void shutdown() {
|
||||
savePendingChanges();
|
||||
FileSaver.shutdown();
|
||||
}
|
||||
|
||||
/// Saves deferred launcher state changes.
|
||||
public static void savePendingChanges() {
|
||||
if (launcherState != null && launcherState.isSavable() && launcherState.isSavePending()) {
|
||||
launcherState.setSavePending(false);
|
||||
STATE_FILE.save(launcherState);
|
||||
}
|
||||
}
|
||||
|
||||
/// The local directory storing per-workspace configuration files.
|
||||
private static final Path LOCAL_CONFIG_FILES_DIRECTORY = Metadata.HMCL_LOCAL_HOME.resolve("config");
|
||||
|
||||
|
||||
@@ -28,12 +28,19 @@ import org.glavo.monetfx.ColorScheme;
|
||||
import org.jackhuang.hmcl.theme.ResolvedTheme;
|
||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
@@ -60,8 +67,23 @@ public final class StyleSheets {
|
||||
});
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
@@ -111,7 +133,7 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString());
|
||||
return toStyleSheetUri(builder.toString(), defaultCss);
|
||||
}
|
||||
|
||||
private static String getBrightnessStyleSheet() {
|
||||
@@ -162,7 +184,7 @@ public final class StyleSheets {
|
||||
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
|
||||
|
||||
builder.append("}\n");
|
||||
return toStyleSheetUri(builder.toString());
|
||||
return toStyleSheetUri(builder.toString(), blueCss);
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -36,7 +36,9 @@ import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -71,7 +73,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -96,12 +97,12 @@ public final class Controllers {
|
||||
public static final int MIN_CONTENT_HEIGHT = 450 + 2 + 40; // bg height + border width*2 + toolbar height
|
||||
public static final int MIN_WIDTH = MIN_CONTENT_WIDTH + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
public static final Rectangle2D PRIMARY_SCREEN_BOUNDS = Screen.getPrimary().getBounds();
|
||||
public static final Screen SCREEN = Screen.getPrimary();
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
private static final DoubleProperty contentX = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentY = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentWidth = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentHeight = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageX = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageY = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
@@ -132,18 +133,14 @@ public final class Controllers {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
public static @Nullable Stage getStage() {
|
||||
public static Scene getScene() {
|
||||
return scene;
|
||||
}
|
||||
|
||||
public static Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
|
||||
public static ReadOnlyDoubleProperty windowWidthProperty() {
|
||||
return contentWidth;
|
||||
}
|
||||
|
||||
public static ReadOnlyDoubleProperty windowHeightProperty() {
|
||||
return contentHeight;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
if (versionPage == null) {
|
||||
@@ -217,8 +214,64 @@ public final class Controllers {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
public static void saveWindowStates() {
|
||||
saveWindowBounds();
|
||||
}
|
||||
|
||||
private static void saveWindowBounds() {
|
||||
if (stageX != null) {
|
||||
state().setX(toContentX(stageX.get()) / SCREEN.getBounds().getWidth());
|
||||
}
|
||||
if (stageY != null) {
|
||||
state().setY(toContentY(stageY.get()) / SCREEN.getBounds().getHeight());
|
||||
}
|
||||
if (stageHeight != null) {
|
||||
state().setHeight(toContentHeight(stageHeight.get()));
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
state().setWidth(toContentWidth(stageWidth.get()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
stageSizeChangeListener = null;
|
||||
saveWindowBounds();
|
||||
stageX = null;
|
||||
stageY = null;
|
||||
stageHeight = null;
|
||||
stageWidth = null;
|
||||
}
|
||||
|
||||
private static double toContentX(double stageX) {
|
||||
return stageX + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
}
|
||||
|
||||
private static double toContentY(double stageY) {
|
||||
return stageY + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
}
|
||||
|
||||
private static double toStageX(double contentX) {
|
||||
return contentX - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
}
|
||||
|
||||
private static double toStageY(double contentY) {
|
||||
return contentY - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
}
|
||||
|
||||
private static double toContentWidth(double stageWidth) {
|
||||
return Math.max(0.0, stageWidth - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
}
|
||||
|
||||
private static double toContentHeight(double stageHeight) {
|
||||
return Math.max(0.0, stageHeight - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
}
|
||||
|
||||
private static double toStageWidth(double contentWidth) {
|
||||
return contentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
}
|
||||
|
||||
private static double toStageHeight(double contentHeight) {
|
||||
return contentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
@@ -232,7 +285,7 @@ public final class Controllers {
|
||||
LOG.info("Enable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "true");
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Screen.getPrimary().getOutputScaleX() > 1) {
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|
||||
LOG.info("Disable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "false");
|
||||
}
|
||||
@@ -240,14 +293,52 @@ public final class Controllers {
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
|
||||
DoubleProperty targetProperty;
|
||||
switch (sourceProperty.getName()) {
|
||||
case "x": {
|
||||
targetProperty = stageX;
|
||||
break;
|
||||
}
|
||||
case "y": {
|
||||
targetProperty = stageY;
|
||||
break;
|
||||
}
|
||||
case "width": {
|
||||
targetProperty = stageWidth;
|
||||
break;
|
||||
}
|
||||
case "height": {
|
||||
targetProperty = stageHeight;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
targetProperty = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
|
||||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
|
||||
) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
|
||||
double initContentWidth = Math.max(MIN_CONTENT_WIDTH, state().getWidth());
|
||||
double initContentHeight = Math.max(MIN_CONTENT_HEIGHT, state().getHeight());
|
||||
double initWidth = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
double initHeight = initContentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
double initWidth = toStageWidth(initContentWidth);
|
||||
double initHeight = toStageHeight(initContentHeight);
|
||||
|
||||
{
|
||||
double initContentX = state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
|
||||
double initContentY = state().getY() * PRIMARY_SCREEN_BOUNDS.getHeight();
|
||||
double initContentX = state().getX() * SCREEN.getBounds().getWidth();
|
||||
double initContentY = state().getY() * SCREEN.getBounds().getHeight();
|
||||
|
||||
boolean invalid = true;
|
||||
double border = 20D;
|
||||
@@ -264,65 +355,24 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
initContentX = (0.5D - initContentWidth / PRIMARY_SCREEN_BOUNDS.getWidth() / 2)
|
||||
* PRIMARY_SCREEN_BOUNDS.getWidth();
|
||||
initContentY = (0.5D - initContentHeight / PRIMARY_SCREEN_BOUNDS.getHeight() / 2)
|
||||
* PRIMARY_SCREEN_BOUNDS.getHeight();
|
||||
initContentX = (0.5D - initContentWidth / SCREEN.getBounds().getWidth() / 2)
|
||||
* SCREEN.getBounds().getWidth();
|
||||
initContentY = (0.5D - initContentHeight / SCREEN.getBounds().getHeight() / 2)
|
||||
* SCREEN.getBounds().getHeight();
|
||||
}
|
||||
|
||||
double initX = initContentX - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
double initX = toStageX(initContentX);
|
||||
double initY = toStageY(initContentY);
|
||||
stage.setX(initX);
|
||||
stage.setY(initY);
|
||||
contentX.set(initContentX);
|
||||
contentY.set(initContentY);
|
||||
stageX.set(initX);
|
||||
stageY.set(initY);
|
||||
}
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
contentHeight.set(initContentHeight);
|
||||
contentWidth.set(initContentWidth);
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o;
|
||||
Stage currentStage = property.getBean() instanceof Stage s ? s : null;
|
||||
if (currentStage == null)
|
||||
return;
|
||||
|
||||
boolean saveState = !currentStage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
|| !currentStage.isFullScreen() && !currentStage.isMaximized());
|
||||
|
||||
switch (property.getName()) {
|
||||
case "x" -> {
|
||||
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
contentX.set(value);
|
||||
if (saveState)
|
||||
state().setX(value / PRIMARY_SCREEN_BOUNDS.getWidth());
|
||||
}
|
||||
case "y" -> {
|
||||
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
contentY.set(value);
|
||||
if (saveState)
|
||||
state().setY(value / PRIMARY_SCREEN_BOUNDS.getHeight());
|
||||
}
|
||||
case "width" -> {
|
||||
double value = Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
contentWidth.set(value);
|
||||
if (saveState)
|
||||
state().setWidth(value);
|
||||
}
|
||||
case "height" -> {
|
||||
double value = Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
contentHeight.set(value);
|
||||
if (saveState)
|
||||
state().setHeight(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
stageHeight.set(initHeight);
|
||||
stageWidth.set(initWidth);
|
||||
stage.xProperty().addListener(weakListener);
|
||||
stage.yProperty().addListener(weakListener);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
@@ -535,7 +585,7 @@ public final class Controllers {
|
||||
|
||||
/// Shows a warning that confirms backing up a read-only settings file before overwriting it.
|
||||
///
|
||||
/// @param text the file-specific read-only warning
|
||||
/// @param text the file-specific read-only warning
|
||||
/// @param overwrite the action that backs up and overwrites the file
|
||||
public static void confirmBackupAndOverwrite(String text, ThrowingRunnable overwrite) {
|
||||
dialog(new MessageDialogPane.Builder(
|
||||
@@ -644,22 +694,6 @@ public final class Controllers {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {
|
||||
return FileUtils.toPath(directoryChooser.showDialog(stage));
|
||||
}
|
||||
|
||||
public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showOpenDialog(stage));
|
||||
}
|
||||
|
||||
public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showSaveDialog(stage));
|
||||
}
|
||||
|
||||
public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage));
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
switch (href) {
|
||||
|
||||
@@ -33,7 +33,6 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import kala.encdet.EncodingDetector;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
@@ -49,6 +48,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
@@ -149,7 +149,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
String log;
|
||||
try {
|
||||
log = EncodingDetector.MODERN_WEB.readString(latestLog);
|
||||
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read logs/latest.log", e);
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
|
||||
@@ -164,7 +164,6 @@ public final class LogWindow extends Stage {
|
||||
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final BooleanProperty wrapText = new SimpleBooleanProperty(true);
|
||||
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
|
||||
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
|
||||
private final JFXButton btnAlwaysOnTop = FXUtils.newToggleButton4(SVG.KEEP, 20);
|
||||
@@ -175,6 +174,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())));
|
||||
|
||||
@@ -331,26 +331,11 @@ public final class LogWindow extends Stage {
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
wrapTextProperty().addListener((obs, oldWrap, nowWrap) -> {
|
||||
if (nowWrap) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
} else {
|
||||
maxWidthProperty().unbind();
|
||||
prefWidthProperty().unbind();
|
||||
|
||||
setMaxWidth(Region.USE_PREF_SIZE);
|
||||
setPrefWidth(Region.USE_COMPUTED_SIZE);
|
||||
listView.getStyleClass().remove("no-horizontal-scrollbar");
|
||||
}
|
||||
|
||||
// only for horizontal scrollbar
|
||||
listView.requestLayout();
|
||||
});
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
setPadding(new Insets(2));
|
||||
wrapTextProperty().bind(control.wrapText);
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
@@ -410,10 +395,6 @@ public final class LogWindow extends Stage {
|
||||
autoScrollCheckBox.setSelected(true);
|
||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||
|
||||
JFXCheckBox wrapTextCheckBox = new JFXCheckBox(i18n("logwindow.wrap_text"));
|
||||
wrapTextCheckBox.setSelected(true);
|
||||
control.wrapText.bind(wrapTextCheckBox.selectedProperty());
|
||||
|
||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
||||
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
|
||||
|
||||
@@ -433,7 +414,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
control.getGameProcess().getProcess()
|
||||
.onExit()
|
||||
|
||||
@@ -66,7 +66,6 @@ public enum SVG {
|
||||
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_FILL("M4 20q-.825 0-1.4125-.5875T2 18v-12q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8v10q0 .825-.5875 1.4125T20 20H4Z"),
|
||||
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
|
||||
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
|
||||
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
|
||||
|
||||
@@ -22,8 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -36,6 +34,8 @@ import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Node;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -44,8 +44,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
|
||||
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new JFXSpinner());
|
||||
@@ -59,7 +59,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
// Downgrade update, no need to display changelog
|
||||
return null;
|
||||
|
||||
Document document = Jsoup.parse(WebURL.toURL(url), 30 * 1000);
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -150,7 +151,7 @@ public class AccountListItem extends RadioButton {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("account.skin.upload"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class AnimationUtils {
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static final boolean ENABLED = !settings().isAnimationDisabled();
|
||||
private static final boolean ENABLED = !settings().animationDisabledProperty().get();
|
||||
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
|
||||
|
||||
public static boolean isAnimationEnabled() {
|
||||
|
||||
@@ -120,7 +120,7 @@ public class FileSelector extends HBox {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
|
||||
Path file = Controllers.showOpenDialog(chooser);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
@@ -131,7 +131,7 @@ public class FileSelector extends HBox {
|
||||
private void openDirectoryChooser(JFXTextField customField) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
|
||||
Path dir = Controllers.showDialog(chooser);
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
|
||||
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
@@ -57,9 +57,7 @@ public class LineFileChooserButton extends LineButton {
|
||||
public void fire() {
|
||||
super.fire();
|
||||
|
||||
Scene scene = this.getScene();
|
||||
|
||||
Window owner = scene != null ? scene.getWindow() : null; // TODO: Allow user to set owner stage
|
||||
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
|
||||
String windowTitle = getFileChooserTitle();
|
||||
|
||||
Path initialDirectory = null;
|
||||
|
||||
@@ -88,7 +88,7 @@ public interface TabControl {
|
||||
|
||||
setSelectedIndex(index);
|
||||
|
||||
Tab<?> tab = getModelItem(index);
|
||||
Tab tab = getModelItem(index);
|
||||
if (tab != null) {
|
||||
setSelectedItem(tab);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ public interface TabControl {
|
||||
final int itemCount = getItemCount();
|
||||
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
final Tab<?> value = getModelItem(i);
|
||||
final Tab value = getModelItem(i);
|
||||
if (value != null && value.equals(tab)) {
|
||||
select(i);
|
||||
return;
|
||||
|
||||
@@ -21,12 +21,11 @@ import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -68,18 +67,16 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
center.getChildren().setAll(lblTitle, taskListPane);
|
||||
}
|
||||
|
||||
HBox bottom = new HBox();
|
||||
bottom.setAlignment(Pos.CENTER_LEFT);
|
||||
bottom.setPadding(new Insets(0, 8, 8, 8));
|
||||
bottom.setSpacing(8);
|
||||
BorderPane bottom = new BorderPane();
|
||||
this.setBottom(bottom);
|
||||
bottom.setPadding(new Insets(0, 8, 8, 8));
|
||||
{
|
||||
lblProgress = new Label();
|
||||
Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
bottom.setLeft(lblProgress);
|
||||
|
||||
btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
bottom.getChildren().setAll(lblProgress, spacer, btnCancel);
|
||||
bottom.setRight(btnCancel);
|
||||
}
|
||||
|
||||
setCancel(cancel);
|
||||
|
||||
@@ -21,14 +21,13 @@ import com.jfoenix.controls.JFXButton;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -49,23 +48,18 @@ public class GameDirectoryListItemSkin extends SkinBase<GameDirectoryListItem> {
|
||||
root.setPickOnBounds(false);
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
|
||||
SVGContainer left = (skinnable.isSelected() ? SVG.FOLDER_FILL : SVG.FOLDER).createIcon(20);
|
||||
left.setMouseTransparent(true);
|
||||
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
root.setLeft(left);
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.selectedProperty(), active -> {
|
||||
skinnable.pseudoClassStateChanged(SELECTED, active);
|
||||
|
||||
SVG targetIcon = active ? SVG.FOLDER_FILL : SVG.FOLDER;
|
||||
if (left.getIcon() != targetIcon) {
|
||||
left.setIcon(targetIcon, Motion.SHORT4);
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onClicked(getSkinnable(), () -> GameDirectoryManager.setSelectedGameDirectory(skinnable.getGameDirectory()));
|
||||
|
||||
Node left = SVG.FOLDER.createIcon(20);
|
||||
left.setMouseTransparent(true);
|
||||
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
|
||||
root.setLeft(left);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setPickOnBounds(false);
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
@@ -27,14 +27,11 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
@@ -115,30 +112,6 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
return new InstallersPageSkin(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to display the extension pane.
|
||||
* <p>
|
||||
* This method controls the visibility of the extension pane in the UI.
|
||||
* When this method returns {@code true} and the pane is visible, users can
|
||||
* interact with the features inside it, which includes triggering the
|
||||
* "Reset to Default Name" button handled by {@link #resetDefaultName()}.
|
||||
* </p>
|
||||
*
|
||||
* @return {@code true} if the extension pane should be displayed; {@code false} otherwise.
|
||||
*/
|
||||
protected abstract boolean showExtendPane();
|
||||
|
||||
/**
|
||||
* Resets the name to its default value.
|
||||
* <p>
|
||||
* This method contains the business logic for the "Reset to Default Name" button
|
||||
* located within the extension pane. This logic can only be triggered by the user
|
||||
* clicking the corresponding button after {@link #showExtendPane()} returns
|
||||
* {@code true} and the extension pane is successfully displayed.
|
||||
* </p>
|
||||
*/
|
||||
protected abstract void resetDefaultName();
|
||||
|
||||
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
@@ -154,27 +127,11 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
{
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.getStyleClass().add("card-non-transparent");
|
||||
versionNamePane.setStyle("-fx-padding: 20 16 20 16");
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
HBox.setHgrow(control.txtName, Priority.ALWAYS);
|
||||
|
||||
versionNamePane.getChildren().addAll(new Label(i18n("version.name")), control.txtName);
|
||||
|
||||
if (control.showExtendPane()) {
|
||||
JFXButton clearButton = FXUtils.newToggleButton4(SVG.CLOSE);
|
||||
FXUtils.installFastTooltip(clearButton, i18n("button.clear"));
|
||||
clearButton.disableProperty().bind(control.txtName.textProperty().isEmpty().or(control.txtName.disableProperty()));
|
||||
clearButton.setOnAction(e -> control.txtName.clear());
|
||||
|
||||
JFXButton resetButton = FXUtils.newToggleButton4(SVG.RESTORE);
|
||||
FXUtils.installFastTooltip(resetButton, i18n("button.reset"));
|
||||
resetButton.disableProperty().bind(control.txtName.disableProperty());
|
||||
resetButton.setOnAction(e -> control.resetDefaultName());
|
||||
|
||||
versionNamePane.getChildren().addAll(clearButton, resetButton);
|
||||
}
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
|
||||
root.setTop(versionNamePane);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,13 +111,4 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean showExtendPane() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetDefaultName() {
|
||||
}
|
||||
}
|
||||
@@ -143,15 +143,4 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
txtName.setText(nameBuilder.toString());
|
||||
isNameModifiedByUser = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean showExtendPane() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetDefaultName() {
|
||||
isNameModifiedByUser = false;
|
||||
setTxtNameWithLoaders();
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
selectedFile = Controllers.showOpenDialog(chooser);
|
||||
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -111,7 +112,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
public VanillaInstallWizardProvider(HMCLGameRepository repository) {
|
||||
this.repository = repository;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
this.dependencyManager = repository.getDependency(downloadProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(SettingsMap settings) {
|
||||
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
|
||||
settings.put(ModpackPage.REPOSITORY, repository);
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameBuilder builder = dependencyManager.gameBuilder();
|
||||
|
||||
String name = (String) settings.get("name");
|
||||
builder.name(name);
|
||||
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
|
||||
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!"game".equals(key) && value instanceof RemoteVersion remoteVersion)
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
|
||||
return builder.buildAsync().whenComplete(any -> {
|
||||
repository.refreshVersions();
|
||||
})
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.new_game.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
|
||||
() -> controller.onNext(new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
@@ -117,7 +118,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
fileChooser.setInitialFileName(name.get() + ".zip");
|
||||
}
|
||||
Path file = Controllers.showSaveDialog(fileChooser);
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
|
||||
chooser.setTitle(i18n("settings.game.java_directory.choose"));
|
||||
Path file = Controllers.showOpenDialog(chooser);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
|
||||
@@ -44,7 +44,12 @@ import javafx.stage.FileChooser;
|
||||
import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorStyle;
|
||||
import org.glavo.uuid.UUIDs;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.setting.BackgroundType;
|
||||
import org.jackhuang.hmcl.setting.FontManager;
|
||||
import org.jackhuang.hmcl.setting.LauncherSettings;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.setting.ThemeColorType;
|
||||
import org.jackhuang.hmcl.setting.UserSettings;
|
||||
import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
|
||||
import org.jackhuang.hmcl.theme.BuiltinBackground;
|
||||
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
||||
@@ -342,25 +347,20 @@ public class PersonalizationPage extends StackPane {
|
||||
String defaultPackId = "com.example.hmcl.theme-pack." + UUIDs.toCompactString(UUIDs.generateV7(exportTimestamp));
|
||||
String defaultPackName = LocalDateTime.ofInstant(exportTimestamp, ZoneId.systemDefault()).format(EXPORTED_THEME_NAME_FORMATTER);
|
||||
|
||||
String accountName = null;
|
||||
{
|
||||
var currentAccount = Accounts.getSelectedAccount();
|
||||
if (currentAccount != null) accountName = currentAccount.getProfileName();
|
||||
}
|
||||
if (StringUtils.isBlank(accountName)) accountName = System.getProperty("user.name").trim();
|
||||
String defaultAuthorName = StringUtils.isBlank(accountName) ? "Unknown" : accountName;
|
||||
String userName = System.getProperty("user.name").trim();
|
||||
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
|
||||
|
||||
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.name"),
|
||||
defaultPackName)
|
||||
"")
|
||||
.setPromptText(defaultPackName);
|
||||
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.version"),
|
||||
ThemePackManager.CURRENT_THEME_PACK_VERSION)
|
||||
"")
|
||||
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
|
||||
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.author"),
|
||||
defaultAuthorName)
|
||||
"")
|
||||
.setPromptText(defaultAuthorName);
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
|
||||
@@ -387,7 +387,7 @@ public class PersonalizationPage extends StackPane {
|
||||
chooser.getExtensionFilters().setAll(
|
||||
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
|
||||
|
||||
@Nullable Path output = Controllers.showSaveDialog(chooser);
|
||||
@Nullable Path output = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
|
||||
if (output == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1007,7 +1007,6 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
Label textOpacity = new Label();
|
||||
FXUtils.setLimitWidth(textOpacity, 50);
|
||||
textOpacity.setAlignment(Pos.CENTER);
|
||||
|
||||
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
|
||||
textOpacity.textProperty().bind(valueBinding);
|
||||
@@ -1172,8 +1171,7 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
LineToggleButton animationButton = new LineToggleButton();
|
||||
appearanceList.getContent().add(animationButton);
|
||||
animationButton.setSelected(settings().isAnimationDisabled());
|
||||
FXUtils.onChange(animationButton.selectedProperty(), value -> settings().animationDisabledProperty().set(value));
|
||||
animationButton.selectedProperty().bindBidirectional(settings().animationDisabledProperty());
|
||||
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
|
||||
animationButton.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
|
||||
@@ -22,11 +22,8 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -35,7 +32,6 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
@@ -45,6 +41,7 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
@@ -162,7 +159,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
chooser.getExtensionFilters().setAll(
|
||||
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
|
||||
|
||||
@Nullable Path file = Controllers.showOpenDialog(chooser);
|
||||
@Nullable Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
@@ -418,7 +415,8 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
/// @param themePack the installed theme pack to display
|
||||
private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.InstalledThemePack themePack) {
|
||||
ThemePackManifest manifest = themePack.manifest();
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
|
||||
HBox heading = new HBox(8);
|
||||
heading.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -483,7 +481,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setPrefViewportWidth(520);
|
||||
scrollPane.setPrefViewportHeight(Math.min(360, manifest.themes().size() * 86));
|
||||
scrollPane.maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.55));
|
||||
scrollPane.maxHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.55));
|
||||
|
||||
StackPane body = new StackPane(scrollPane);
|
||||
body.setPadding(new Insets(10, 0, 0, 0));
|
||||
@@ -510,9 +508,6 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
/// Toolbar shown during normal browsing.
|
||||
private final HBox toolbarNormal = new HBox();
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
/// Search input.
|
||||
private final JFXTextField searchField = new JFXTextField();
|
||||
|
||||
@@ -550,20 +545,13 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
pause.setOnFinished(event ->
|
||||
skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
}
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
searchField.clear();
|
||||
pause.stop();
|
||||
|
||||
skinnable.filteredList.setPredicate(null);
|
||||
isSearching.set(false);
|
||||
});
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
|
||||
@@ -598,15 +586,6 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, event -> event.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label();
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.when(isSearching).then(i18n("search.no_results_found")).otherwise("")
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
@@ -618,8 +597,6 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
runInFX(searchField::requestFocus);
|
||||
|
||||
isSearching.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.nio.file.Path;
|
||||
|
||||
/// @author Glavo
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("nbt", "dat", "dat_old") {
|
||||
COMPRESSED("dat", "dat_old") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
return NBTCodec.of().readTag(file);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.terracotta;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
@@ -57,7 +56,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -86,22 +84,15 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final Binding<Boolean> focusedBinding;
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/* FIXME: It's sucked to have such a long logic, containing UI for all states defined in TerracottaState, with unclear control flows.
|
||||
Consider moving UI into multiple files for each state respectively. */
|
||||
public TerracottaControllerPage() {
|
||||
focusedBinding = BindingMapping.of(this.sceneProperty())
|
||||
.flatMap(it -> it != null ? it.windowProperty() : new SimpleObjectProperty<>())
|
||||
.flatMap(it -> it != null ? it.focusedProperty() : null, () -> false);
|
||||
|
||||
FXUtils.onChange(focusedBinding, focused -> {
|
||||
holder.add(FXUtils.observeWeak(() -> {
|
||||
// Run daemon process only if HMCL is focused and is displaying current node.
|
||||
TerracottaManager.switchDaemon(getScene() != null && focused);
|
||||
});
|
||||
TerracottaManager.switchDaemon(getScene() != null && Controllers.getStage().isFocused());
|
||||
}, this.sceneProperty(), Controllers.getStage().focusedProperty()));
|
||||
|
||||
TransitionPane transition = new TransitionPane();
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.Da
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("datapack.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null) {
|
||||
installMultiDataPack(res);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -34,7 +33,6 @@ import javafx.collections.transformation.FilteredList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -53,9 +51,7 @@ 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.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
@@ -72,7 +68,6 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@NotNullByDefault
|
||||
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
@@ -84,15 +79,10 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
private final JFXListView<DataPackInfoObject> listView;
|
||||
private final FilteredList<DataPackInfoObject> filteredList;
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
|
||||
private final JFXTextField searchField;
|
||||
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
|
||||
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
|
||||
final Consumer<Integer> toggleSelect;
|
||||
|
||||
@@ -158,21 +148,16 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchPause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
filteredList.setPredicate(null);
|
||||
|
||||
isSearching.set(false);
|
||||
searchField.clear();
|
||||
});
|
||||
FXUtils.onEscPressed(searchField, closeSearchBar::fire);
|
||||
searchBar.getChildren().addAll(searchField, closeSearchBar);
|
||||
@@ -208,23 +193,6 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
|
||||
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("datapack.empty"));
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (isSearching.get()) {
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return i18n("datapack.empty");
|
||||
}
|
||||
},
|
||||
isSearching)
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
this.listView.setItems(filteredList);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
if (getSkinnable().isFailed()) {
|
||||
return i18n("download.failed.refresh");
|
||||
} else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) {
|
||||
return i18n("search.no_results_found");
|
||||
return i18n("download.failed.no_results_found");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -169,7 +171,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
fileChooser.setTitle(i18n("button.save_as"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension));
|
||||
fileChooser.setInitialFileName(file.file().filename());
|
||||
Path dest = Controllers.showSaveDialog(fileChooser);
|
||||
Path dest = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (dest == null) {
|
||||
return;
|
||||
}
|
||||
@@ -470,9 +472,6 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
case QUILT:
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
case LEGACY_FABRIC:
|
||||
content.addTag(i18n("install.installer.legacyfabric"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,8 +557,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.setActions(downloadButton, saveAsButton, cancelButton);
|
||||
}
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
|
||||
this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
|
||||
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));
|
||||
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@ import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
@@ -52,9 +51,9 @@ import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
|
||||
import org.jackhuang.hmcl.ui.directory.GameDirectoryPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
@@ -104,8 +103,10 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame)
|
||||
.addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack)
|
||||
.addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings);
|
||||
FXUtils.setLimitHeight(bottomLeftCornerList, 40 + 12 * 2);
|
||||
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2);
|
||||
setLeft(pane, bottomLeftCornerList);
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
GameDirectoryManager.registerVersionsListener(this::loadVersions);
|
||||
|
||||
setOnFailedAction(e -> Versions.addNewGame());
|
||||
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
}
|
||||
|
||||
@FXThread
|
||||
@@ -234,7 +235,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("install.new_game"), SVG.DOWNLOAD, Versions::addNewGame), createToolbarButton2(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
|
||||
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
|
||||
|
||||
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
public void installOffline() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
|
||||
Path file = Controllers.showOpenDialog(chooser);
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -50,6 +52,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.GameInstanceLoadable, PageAware {
|
||||
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private ModManager modManager;
|
||||
@@ -88,16 +92,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId);
|
||||
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
|
||||
modded.set(analyzer.hasModLoader());
|
||||
loadMods(repository.getModManager(instanceId));
|
||||
}
|
||||
|
||||
private void loadMods(ModManager modManager) {
|
||||
setLoading(true);
|
||||
|
||||
if (this.modManager != modManager) {
|
||||
getItems().clear();
|
||||
}
|
||||
this.modManager = modManager;
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
lock.lock();
|
||||
@@ -110,10 +112,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
lock.unlock();
|
||||
}
|
||||
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
|
||||
if (this.modManager != modManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSupportedLoaders(modManager);
|
||||
|
||||
if (exception == null) {
|
||||
@@ -175,7 +173,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res == null) return;
|
||||
|
||||
@@ -238,10 +236,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
public void checkUpdates(Collection<LocalModFile> mods) {
|
||||
Objects.requireNonNull(mods);
|
||||
if (isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
@@ -284,6 +278,18 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isModded() {
|
||||
return modded.get();
|
||||
}
|
||||
|
||||
public BooleanProperty moddedProperty() {
|
||||
return modded;
|
||||
}
|
||||
|
||||
public void setModded(boolean modded) {
|
||||
this.modded.set(modded);
|
||||
}
|
||||
|
||||
public GameDirectory getGameDirectory() {
|
||||
return this.repository.getGameDirectory();
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ import com.jfoenix.controls.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -37,12 +35,12 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.*;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
@@ -63,7 +61,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
@@ -83,7 +80,6 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@NotNullByDefault
|
||||
final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
@@ -92,14 +88,10 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
private final HBox toolbarSelecting;
|
||||
|
||||
private final JFXListView<ModInfoObject> listView;
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
private final JFXTextField searchField;
|
||||
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
@FXThread
|
||||
private boolean isSearching = false;
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -109,7 +101,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
pane.getChildren().setAll(root);
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
@@ -127,22 +118,19 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchPause.setOnFinished(e -> search());
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> search());
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
isSearching = false;
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
@@ -204,7 +192,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
changeToolbar(isSearching ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
@@ -231,26 +219,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("mods.empty"));
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (isSearching.get()) {
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return i18n("mods.empty");
|
||||
}
|
||||
},
|
||||
isSearching)
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
skinnable.getItems().addListener((ListChangeListener<? super ModInfoObject>) c -> {
|
||||
if (isSearching.get()) {
|
||||
if (isSearching) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
@@ -271,6 +242,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
Label label = new Label(i18n("mods.not_modded"));
|
||||
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
|
||||
if (modded) pane.getChildren().setAll(root);
|
||||
else pane.getChildren().setAll(label);
|
||||
});
|
||||
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
@@ -285,7 +264,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching.set(true);
|
||||
isSearching = true;
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
@@ -400,14 +379,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
titleContainer.setPadding(new Insets(0, 0, 12, 0));
|
||||
|
||||
DoubleBinding widthBinding = Controllers.windowWidthProperty().multiply(0.7);
|
||||
prefWidthProperty().bind(widthBinding);
|
||||
maxWidthProperty().bind(widthBinding);
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
@@ -416,13 +392,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
StringJoiner subtitle = new StringJoiner("\n");
|
||||
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
StringJoiner subtitle = new StringJoiner(" | ");
|
||||
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
|
||||
subtitle.add(modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
|
||||
subtitle.add(modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
@@ -442,7 +418,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.*;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
@@ -56,7 +57,7 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -175,7 +176,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("resourcepack.add"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
}
|
||||
@@ -256,7 +257,6 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
private static final class ResourcePackListPageSkin extends SkinBase<ResourcePackListPage> {
|
||||
private final JFXListView<ResourcePackInfoObject> listView;
|
||||
private final JFXTextField searchField = new JFXTextField();
|
||||
@@ -266,11 +266,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
private final HBox toolbarNormal = new HBox();
|
||||
private final HBox toolbarSelecting = new HBox();
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
private boolean isSearching;
|
||||
|
||||
private ResourcePackListPageSkin(ResourcePackListPage control) {
|
||||
super(control);
|
||||
@@ -313,22 +309,19 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 5));
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchPause.setOnFinished(e -> search());
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> search());
|
||||
FXUtils.onChange(searchField.textProperty(), newValue -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
isSearching = false;
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
@@ -353,7 +346,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
changeToolbar(isSearching ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
@@ -378,23 +371,6 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
|
||||
listView.setCellFactory(x -> new ResourcePackListCell(listView, control));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("resourcepack.empty"));
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (isSearching.get()) {
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return i18n("resourcepack.empty");
|
||||
}
|
||||
},
|
||||
isSearching)
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
Bindings.bindContent(listView.getItems(), control.getItems());
|
||||
|
||||
listView.setOnContextMenuRequested(event -> {
|
||||
@@ -427,7 +403,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching.set(true);
|
||||
isSearching = true;
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
@@ -591,7 +567,8 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
|
||||
ImageContainer imageContainer = new ImageContainer(40);
|
||||
imageContainer.setImage(packInfoObject.getIcon());
|
||||
@@ -617,7 +594,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
FXUtils.onChange(description.heightProperty(), newVal -> {
|
||||
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
fileChooser.setTitle(i18n("schematics.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
i18n("extension.schematic"), "*.litematic"));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
}
|
||||
@@ -636,12 +636,6 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
|
||||
SchematicsPageSkin() {
|
||||
super(SchematicsPage.this);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("schematics.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -75,7 +76,7 @@ public class VersionIconDialog extends DialogPane {
|
||||
private void exploreIcon() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile != null) {
|
||||
try {
|
||||
repository.setVersionIconFile(versionId, selectedFile);
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
@@ -28,10 +29,8 @@ 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.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.QuickPlayOption;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -93,7 +92,6 @@ public final class Versions {
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(downloadURLs, modpack)
|
||||
.setName(file.name())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
ModpackInstallWizardProvider installWizardProvider;
|
||||
@@ -265,7 +263,7 @@ public final class Versions {
|
||||
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
|
||||
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
|
||||
Path file = Controllers.showSaveDialog(chooser);
|
||||
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
|
||||
String defaultExt = getDefaultScriptExtension();
|
||||
|
||||
@@ -24,7 +24,6 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
@@ -163,12 +162,6 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
|
||||
WorldBackupsPageSkin() {
|
||||
super(WorldBackupsPage.this);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("world.backup.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -639,7 +639,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||
fileChooser.setInitialFileName("icon.png");
|
||||
|
||||
Path iconPath = Controllers.showOpenDialog(fileChooser);
|
||||
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
|
||||
if (iconPath == null) return;
|
||||
|
||||
Image image;
|
||||
|
||||
@@ -27,7 +27,6 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
@@ -141,7 +140,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("world.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res == null || res.isEmpty()) return;
|
||||
installWorld(res.get(0));
|
||||
@@ -217,12 +216,6 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
WorldListPageSkin() {
|
||||
super(WorldListPage.this);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("world.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -74,7 +74,7 @@ public final class WorldManageUIUtils {
|
||||
fileChooser.setTitle(i18n("world.export.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
|
||||
Path file = Controllers.showSaveDialog(fileChooser);
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ 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.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -44,10 +43,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
@@ -128,21 +127,23 @@ public final class UpdateHandler {
|
||||
throw new IOException("Current JAR is not verified");
|
||||
}
|
||||
|
||||
var latch = new CountDownLatch(1);
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
SettingsManager.savePendingChanges();
|
||||
Controllers.saveWindowStates();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
future.get();
|
||||
} catch (ExecutionException | InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
@@ -221,35 +222,13 @@ public final class UpdateHandler {
|
||||
commandline.add("-jar");
|
||||
commandline.add(jar.toAbsolutePath().toString());
|
||||
commandline.addAll(Arrays.asList(appArgs));
|
||||
LOG.info("Starting process: " + maskCommandline(commandline));
|
||||
LOG.info("Starting process: " + commandline);
|
||||
new ProcessBuilder(commandline)
|
||||
.directory(Paths.get("").toAbsolutePath().toFile())
|
||||
.inheritIO()
|
||||
.start();
|
||||
}
|
||||
|
||||
private static String maskCommandline(List<String> commandline) {
|
||||
return commandline.stream().map(str -> {
|
||||
if (str.startsWith("-D")) {
|
||||
int eqIdx = str.indexOf('=');
|
||||
if (eqIdx != -1) {
|
||||
String key = str.substring(2, eqIdx);
|
||||
String value = str.substring(eqIdx + 1);
|
||||
if (key.contains("http.proxy") ||
|
||||
key.startsWith("https.proxy") ||
|
||||
key.startsWith("socksProxy") ||
|
||||
key.equals("hmcl.microsoft.auth.id") ||
|
||||
key.equals("hmcl.curseforge.apikey")
|
||||
) {
|
||||
return "-D" + key + "=" + (value.isEmpty() ? "" : value.charAt(0) + "*".repeat(value.length() - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
private static Optional<Path> tryRename(Path path, String newVersion) {
|
||||
String filename = path.getFileName().toString();
|
||||
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
|
||||
|
||||
@@ -21,7 +21,6 @@ import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import org.jackhuang.hmcl.countly.CrashReport;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.ui.CrashWindow;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -102,7 +101,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
LOG.error("Unable to handle uncaught exception", handlingException);
|
||||
}
|
||||
|
||||
SettingsManager.shutdown();
|
||||
FileSaver.shutdown();
|
||||
LOG.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -399,6 +399,7 @@ download.external_link=Visit Download Website
|
||||
download.failed=Failed to download "%1$s", response code: %2$d.
|
||||
download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
download.failed.no_results_found=No results found
|
||||
download.failed.refresh=Failed to fetch version list. Please click here to retry.
|
||||
download.game=New Game
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -544,6 +545,9 @@ game.crash.reason.modmixin_failure=The game crashed because some mods failed to
|
||||
This generally means that the mod has a bug or is incompatible with the current environment.\n\
|
||||
\n\
|
||||
You can check the log to find the error mod.
|
||||
game.crash.reason.mod_repeat_installation=The game crashed because of duplicate mods.\n\
|
||||
\n\
|
||||
Each mod can only be installed once. Please delete the duplicate mod and then relaunch the game.
|
||||
game.crash.reason.forge_error=Forge/NeoForge may have provided error information.\n\
|
||||
\n\
|
||||
You can view the log and make corresponding processing according to the log information in the error report.\n\
|
||||
@@ -944,7 +948,6 @@ logwindow.terminate_game=Kill Game Process
|
||||
logwindow.title=Log
|
||||
logwindow.help=You can navigate to the HMCL community and find others for help.
|
||||
logwindow.autoscroll=Auto-scroll
|
||||
logwindow.wrap_text=Wrap Text
|
||||
logwindow.export_game_crash_logs=Export Crash Logs
|
||||
logwindow.export_dump=Export Game Stack Dump
|
||||
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help.
|
||||
@@ -1130,14 +1133,14 @@ modrinth.category.vanilla-like=Vanilla-like
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Worldgen
|
||||
modrinth.category.8x-=8x or lower
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x or higher
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add=Add
|
||||
@@ -1148,7 +1151,6 @@ mods.disable=Disable
|
||||
mods.download=Download Mod
|
||||
mods.download.title=Download Mod - %1s
|
||||
mods.download.recommend=Recommended Mod Version - Minecraft %1s
|
||||
mods.empty=No Mods
|
||||
mods.enable=Enable
|
||||
mods.game.version=Game Version
|
||||
mods.manage=Mods
|
||||
@@ -1182,7 +1184,6 @@ nbt.title=View File - %s
|
||||
datapack=Data Packs
|
||||
datapack.add=Add
|
||||
datapack.add.title=Choose data pack archive you want to add
|
||||
datapack.empty=No Data Packs
|
||||
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
|
||||
datapack.title=World [%s] - Data Packs
|
||||
|
||||
@@ -1201,7 +1202,6 @@ world.backup.create.new_one=New Backup
|
||||
world.backup.create.failed=Failed to create backup.\n%s
|
||||
world.backup.create.success=Successfully created a new backup: %s
|
||||
world.backup.delete=Delete this backup
|
||||
world.backup.empty=No Backup
|
||||
world.backup.processing=Backing up ...
|
||||
world.chunkbase=Chunk Base
|
||||
world.chunkbase.end_city=End City
|
||||
@@ -1221,7 +1221,6 @@ world.delete=Delete the World
|
||||
world.delete.failed=Failed to delete world.\n%s
|
||||
world.download=Download
|
||||
world.download.title=Download World - %1s
|
||||
world.empty=No Worlds
|
||||
world.export=Export the World
|
||||
world.export.title=Choose the directory for this exported world
|
||||
world.export.location=Save As
|
||||
@@ -1296,7 +1295,6 @@ resourcepack.add.title=Choose resource pack archive you want to add
|
||||
resourcepack.delete.failed=Failed to delete resource pack
|
||||
resourcepack.download=Download
|
||||
resourcepack.download.title=Download Resource Pack - %1s
|
||||
resourcepack.empty=No Resource Packs
|
||||
resourcepack.manage=Resource Packs
|
||||
resourcepack.update_in_modpack.warning=Updating resource packs in a modpack can lead to irreparable results, possibly leading to incorrect textures and sounds. Are you sure you want to update?
|
||||
resourcepack.warning.invalid=Invalid pack meta
|
||||
@@ -1319,7 +1317,6 @@ schematics.create_directory.failed=Failed to create directory
|
||||
schematics.create_directory.failed.already_exists=Directory already exists
|
||||
schematics.create_directory.failed.empty_name=Name cannot be empty
|
||||
schematics.create_directory.failed.invalid_name=Name contains invalid characters
|
||||
schematics.empty=No Schematics
|
||||
schematics.info.description=Description
|
||||
schematics.info.enclosing_size=Enclosing Size
|
||||
schematics.info.name=Name
|
||||
@@ -1341,7 +1338,6 @@ search.sort=Sort By
|
||||
search.first_page=First
|
||||
search.previous_page=Previous
|
||||
search.next_page=Next
|
||||
search.no_results_found=No results found
|
||||
search.last_page=Last
|
||||
search.page_n=%1$d / %2$s
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ extension.command=سكريبت شيل macOS
|
||||
|
||||
extension.datapack=أرشيف حزمة البيانات
|
||||
extension.mod=ملف مود
|
||||
extension.modloader.installer=مثبّت محمّل المودات
|
||||
extension.modloader=مثبّت محمّل المودات
|
||||
extension.resourcepack=أرشيف حزمة الموارد
|
||||
extension.schematic=ملف مخطط
|
||||
extension.world=أرشيف العالم
|
||||
@@ -508,6 +508,9 @@ game.crash.reason.modmixin_failure=تعطّلت اللعبة لأن بعض ال
|
||||
يعني هذا عموماً أن المود يحتوي على خطأ أو غير متوافق مع البيئة الحالية.\n\
|
||||
\n\
|
||||
يمكنك مراجعة السجل للعثور على المود المسبب للخطأ.
|
||||
game.crash.reason.mod_repeat_installation=تعطّلت اللعبة بسبب مودات مكررة.\n\
|
||||
\n\
|
||||
يمكن تثبيت كل مود مرة واحدة فقط. يُرجى حذف المود المكرر ثم إعادة تشغيل اللعبة.
|
||||
game.crash.reason.forge_error=قد يكون Forge/NeoForge قد أعطى معلومات عن الخطأ.\n\
|
||||
\n\
|
||||
يمكنك مراجعة السجل ومعالجة المشكلة وفقاً لمعلومات السجل في تقرير الخطأ.\n\
|
||||
|
||||
@@ -110,6 +110,7 @@ account.methods.microsoft.purchase=Comprar Minecraft
|
||||
account.methods.microsoft.snapshot.website=Sitio web oficial
|
||||
account.methods.offline=Sin conexión
|
||||
account.methods.offline.name.special_characters=Utilice solo letras, números y guiones bajos (máximo 16 caracteres)
|
||||
account.methods.offline.name.invalid.tip=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.
|
||||
account.methods.offline.name.invalid=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.\n\
|
||||
\n\
|
||||
\ · Algunos nombres de usuario legítimos: HuangYu, huang_Yu, Huang_Yu_123;\n\
|
||||
@@ -576,6 +577,9 @@ game.crash.reason.modmixin_failure=El juego se ha colgado debido a que algunos m
|
||||
Esto generalmente significa que el mod tiene un error o no es compatible con el entorno actual. \n\
|
||||
\n\
|
||||
Puede consultar el registro para ver si hay un mod incorrecto.
|
||||
game.crash.reason.mod_repeat_installation=El juego se ha colgado debido a los mods duplicados.\n\
|
||||
\n\
|
||||
Cada mod sólo puede instalarse una vez. Elimine el mod duplicado y vuelva a ejecutar el juego.
|
||||
game.crash.reason.forge_error=Forge/NeoForge puede haber proporcionado información errónea.\n\
|
||||
\n\
|
||||
Puede ver el registro y realizar las acciones correspondientes de acuerdo con la información de registro en el informe de errores.\n\
|
||||
@@ -1033,8 +1037,14 @@ modrinth.category.vanilla-like=Similar a la vainilla
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Generación de mundos
|
||||
modrinth.category.8x-=8x o más bajo
|
||||
modrinth.category.512x+=512x o mayor
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add.failed=No se ha podido añadir el mod %s.
|
||||
|
||||
@@ -363,6 +363,7 @@ game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
|
||||
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
|
||||
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
|
||||
game.crash.reason.mod_repeat_installation=現在のゲームには複数の同一の Mod が繰り返しインストールされており、各 Mod は 1 回しか表示できません。ゲームを開始する前に繰り返しの Mod を削除してください。
|
||||
game.crash.reason.forge_error=Forge がエラー メッセージを表示した可能性があります。 \nログを表示し、エラー レポートのログ情報に従って対応するアクションを実行できます。 \nエラー メッセージが表示されない場合は、エラー レポートをチェックして、エラーがどのように発生したかを知ることができます。\n%1$s
|
||||
game.crash.reason.mod_resolution0=mod の問題により、現在のゲームを続行できません。 \n間違った mod のログを確認できます。
|
||||
game.crash.reason.java_version_is_too_high=Java のバージョンが高すぎて実行を継続できないため、現在のゲームがクラッシュしました。 \nゲームを起動する前に、グローバル ゲーム設定またはゲーム固有の設定の Java パス タブで Java の以前のバージョンに切り替えてください。 \nそうでない場合は、<a href="https://www.java.com/download/">java.com (Java8)</a> または <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java17)</a> およびその他のプラットフォームをダウンロードしてインストールします (インストール後にランチャーを再起動します)。
|
||||
@@ -612,6 +613,7 @@ modpack.files.resourcepacks=リソース/テクスチャパック
|
||||
modpack.files.saves=保存されたマップ
|
||||
modpack.files.scripts=MineTweaker構成
|
||||
modpack.files.servers_dat=サーバーリスト
|
||||
modpack.install=%s modpackをインストールします
|
||||
modpack.installing=modpackのインストール
|
||||
modpack.introduction= Curse, Modrinth, MultiMC, MCBBS Modpackがサポートされています。
|
||||
modpack.invalid=無効なmodpackファイル。
|
||||
@@ -672,8 +674,6 @@ modrinth.category.utility=ユーティリティ
|
||||
modrinth.category.worldgen=Worldgen
|
||||
modrinth.category.datapack=Datapack
|
||||
modrinth.category.folia=Folia
|
||||
modrinth.category.8x-=8x以下
|
||||
modrinth.category.512x+=512x以上
|
||||
|
||||
mods=Mods
|
||||
mods.add.failed=mods %s の追加に失敗しました。
|
||||
|
||||
@@ -903,6 +903,7 @@ world.add.invalid=無可辨此緊囊
|
||||
world.backup=司備
|
||||
world.backup.create.new_one=建新備
|
||||
world.backup.create.failed=建備未成。\n%s
|
||||
world.backup.create.locked=是生界方見用,須閉戲而後行。
|
||||
world.backup.create.success=新備既成:%s
|
||||
world.backup.delete=刪是備
|
||||
world.backup.processing=方備……
|
||||
|
||||
@@ -111,6 +111,7 @@ account.methods.microsoft.purchase=Купить Minecraft
|
||||
account.methods.microsoft.snapshot.website=Официальный сайт
|
||||
account.methods.offline=Офлайн
|
||||
account.methods.offline.name.special_characters=Использовать только буквы, цифры и подчеркивания (максимум 16 символов)
|
||||
account.methods.offline.name.invalid.tip=Для имени пользователя рекомендуется использовать только английские буквы, цифры и подчеркивания, а его длина не должна превышать 16 символов.
|
||||
account.methods.offline.name.invalid=Для имени пользователя рекомендуется использовать только английские буквы, цифры и подчеркивания, а его длина не должна превышать 16 символов.\n\
|
||||
\n\
|
||||
\ · Законный: HuangYu, huang_Yu, Huang_Yu_123;\n\
|
||||
@@ -472,6 +473,9 @@ game.crash.reason.modmixin_failure=Игра отказала из-за нево
|
||||
Обычно это означает, что мод содержит ошибку или несовместим с текущей средой.\n\
|
||||
\n\
|
||||
Вы можете проверить журнал на наличие неправильного мода.
|
||||
game.crash.reason.mod_repeat_installation=Игра отказала из-за дублирования модов.\n\
|
||||
\n\
|
||||
Каждый мод может быть установлен только один раз. Пожалуйста, удалите дублирующий мод, а затем заново запустите игру.
|
||||
game.crash.reason.need_jdk11=Игра отказала из-за неподходящей версии Java.\n\
|
||||
\n\
|
||||
Вам необходимо загрузить и установить Java 11, а также указать ее в разделе «Глобальные настройки / Раздельные настройки для сборки → Java».
|
||||
@@ -1026,8 +1030,14 @@ modrinth.category.vanilla-like=Ванильное
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Генерация мира
|
||||
modrinth.category.8x-=8x и ниже
|
||||
modrinth.category.512x+=512x и выше
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Моды
|
||||
mods.add.failed=Не удалось установить мод %s.
|
||||
|
||||
@@ -447,6 +447,8 @@ game.crash.reason.duplicated_mod=Гра не може продовжувати
|
||||
game.crash.reason.modmixin_failure=Гра зазнала збою, оскільки деякі моди не вдалося ввести. \n\
|
||||
Це, як правило, означає, що мод має помилку або несумісний з поточним середовищем. \n\
|
||||
Ви можете перевірити журнал, щоб знайти помилковий мод.
|
||||
game.crash.reason.mod_repeat_installation=Гра зазнала збою через дублікати модів. \n\
|
||||
Кожен мод можна встановити лише один раз. Видаліть дублікат мода та знову запустіть гру.
|
||||
game.crash.reason.forge_error=Forge/NeoForge може надати інформацію про помилку. \n\
|
||||
Ви можете переглянути журнал та відповідно обробити його відповідно до інформації про помилку в звіті. \n\
|
||||
Якщо ви не бачите повідомлення про помилку, ви можете переглянути звіт про помилку, щоб зрозуміти, як сталася помилка. \n\
|
||||
@@ -976,14 +978,14 @@ modrinth.category.vanilla-like=Схожий на ваніль
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Генерація світу
|
||||
modrinth.category.8x-=×8 і менше
|
||||
modrinth.category.16x=×16
|
||||
modrinth.category.32x=×32
|
||||
modrinth.category.48x=×48
|
||||
modrinth.category.64x=×64
|
||||
modrinth.category.128x=×128
|
||||
modrinth.category.256x=×256
|
||||
modrinth.category.512x+=×512 і більше
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Моди
|
||||
mods.add.failed=Не вдалося додати мод %s.
|
||||
|
||||
@@ -334,7 +334,7 @@ curse.category.9049=精緻儲存 (Refined Storage)
|
||||
curse.category.10754=農夫樂事 (Farmer's Delight)
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
|
||||
curse.category.5244=字體包
|
||||
curse.category.5244=字體包
|
||||
curse.category.5193=封包
|
||||
curse.category.399=蒸汽朋克
|
||||
curse.category.396=128x
|
||||
@@ -397,6 +397,7 @@ download.external_link=開啟下載網站
|
||||
download.failed=下載失敗:%1$s\n錯誤碼:%2$d
|
||||
download.failed.empty=沒有可供安裝的版本,按一下此處返回。
|
||||
download.failed.no_code=下載失敗
|
||||
download.failed.no_results_found=搜尋無結果
|
||||
download.failed.refresh=載入版本清單失敗,按一下此處重試。
|
||||
download.game=新遊戲
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -493,6 +494,7 @@ game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.mod_repeat_installation=目前遊戲由於重複安裝了多個相同的模組,無法繼續執行。\n每個模組只能出現一次,請刪除重複的模組,然後再啟動遊戲。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已經提供了錯誤資訊。\n你可以查看日誌,並根據錯誤報告中的日誌資訊進行對應處。\n如果沒有看到報錯資訊,可以查看錯誤報告了解錯誤具體是如何發生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=目前遊戲由於一些模組出現問題,無法繼續執行。\n你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
|
||||
@@ -753,7 +755,6 @@ logwindow.terminate_game=結束遊戲處理程式
|
||||
logwindow.title=日誌
|
||||
logwindow.help=你可以前往 HMCL 社群,向他人尋求幫助
|
||||
logwindow.autoscroll=自動滾動
|
||||
logwindow.wrap_text=自動換行
|
||||
logwindow.export_game_crash_logs=匯出遊戲崩潰資訊
|
||||
logwindow.export_dump=匯出遊戲執行堆疊
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用於建立遊戲執行堆疊的相依元件。請前往 Discord 或 HMCL QQ 群尋求幫助。
|
||||
@@ -937,14 +938,6 @@ modrinth.category.vanilla-like=類原生
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
modrinth.category.8x-=8× 以下
|
||||
modrinth.category.16x=16×
|
||||
modrinth.category.32x=32×
|
||||
modrinth.category.48x=48×
|
||||
modrinth.category.64x=64×
|
||||
modrinth.category.128x=128×
|
||||
modrinth.category.256x=256×
|
||||
modrinth.category.512x+=512× 以上
|
||||
|
||||
mods=模組
|
||||
mods.add=新增模組
|
||||
@@ -955,7 +948,6 @@ mods.disable=停用
|
||||
mods.download=模組下載
|
||||
mods.download.title=模組下載 - %1s
|
||||
mods.download.recommend=推薦版本 - Minecraft %1s
|
||||
mods.empty=沒有模組
|
||||
mods.enable=啟用
|
||||
mods.game.version=遊戲版本
|
||||
mods.manage=模組管理
|
||||
@@ -989,7 +981,6 @@ nbt.title=查看檔案 - %s
|
||||
datapack=資料包
|
||||
datapack.add=新增資料包
|
||||
datapack.add.title=選取要新增的資料包壓縮檔
|
||||
datapack.empty=沒有資料包
|
||||
datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包
|
||||
datapack.title=世界 [%s] - 資料包
|
||||
|
||||
@@ -1008,7 +999,6 @@ world.backup.create.new_one=建立新備份
|
||||
world.backup.create.failed=建立備份失敗。\n%s
|
||||
world.backup.create.success=成功建立新備份:%s
|
||||
world.backup.delete=刪除此備份
|
||||
world.backup.empty=沒有備份
|
||||
world.backup.processing=正在備份中……
|
||||
world.chunkbase=世界地圖
|
||||
world.chunkbase.end_city=終界城地圖
|
||||
@@ -1028,7 +1018,6 @@ world.delete=刪除此世界
|
||||
world.delete.failed=刪除世界失敗。\n%s
|
||||
world.download=下載世界
|
||||
world.download.title=世界下載 - %1s
|
||||
world.empty=沒有世界
|
||||
world.export=匯出此世界
|
||||
world.export.title=選取該世界的儲存位置
|
||||
world.export.location=儲存到
|
||||
@@ -1099,7 +1088,6 @@ resourcepack.add.title=選取要新增的資源包壓縮檔
|
||||
resourcepack.delete.failed=刪除資源包失敗
|
||||
resourcepack.download=下載資源包
|
||||
resourcepack.download.title=資源包下載 - %1s
|
||||
resourcepack.empty=沒有資源包
|
||||
resourcepack.manage=資源包管理
|
||||
resourcepack.update_in_modpack.warning=更新模組包中的資源包可能導致模組包損壞,使遊戲紋理和音效異常。你確定要更新嗎?
|
||||
resourcepack.warning.invalid=資源包中繼資料無效
|
||||
@@ -1122,7 +1110,6 @@ schematics.create_directory.failed=建立目錄失敗
|
||||
schematics.create_directory.failed.already_exists=目錄已存在
|
||||
schematics.create_directory.failed.empty_name=名稱不能為空
|
||||
schematics.create_directory.failed.invalid_name=名稱中包含無效字元
|
||||
schematics.empty=沒有投影檔
|
||||
schematics.info.description=描述
|
||||
schematics.info.enclosing_size=框大小
|
||||
schematics.info.name=名稱
|
||||
@@ -1144,7 +1131,6 @@ search.sort=排序
|
||||
search.first_page=第一頁
|
||||
search.previous_page=上一頁
|
||||
search.next_page=下一頁
|
||||
search.no_results_found=搜尋無結果
|
||||
search.last_page=最後一頁
|
||||
search.page_n=%d / %s
|
||||
|
||||
|
||||
@@ -399,6 +399,7 @@ download.external_link=打开下载网站
|
||||
download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。
|
||||
download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助)
|
||||
download.failed.no_code=下载失败
|
||||
download.failed.no_results_found=搜索无结果
|
||||
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
|
||||
download.game=新游戏
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
@@ -759,7 +760,6 @@ logwindow.terminate_game=结束游戏进程
|
||||
logwindow.title=日志
|
||||
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
||||
logwindow.autoscroll=自动滚动
|
||||
logwindow.wrap_text=自动换行
|
||||
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||
logwindow.export_dump=导出游戏运行栈
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 寻求帮助。
|
||||
@@ -943,8 +943,6 @@ modrinth.category.vanilla-like=类原生
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
modrinth.category.8x-=8x 或更低
|
||||
modrinth.category.512x+=512x 或更高
|
||||
|
||||
mods=模组
|
||||
mods.add=添加模组
|
||||
@@ -955,7 +953,6 @@ mods.disable=禁用
|
||||
mods.download=模组下载
|
||||
mods.download.title=模组下载 - %1s
|
||||
mods.download.recommend=推荐版本 - Minecraft %1s
|
||||
mods.empty=没有模组
|
||||
mods.enable=启用
|
||||
mods.game.version=游戏版本
|
||||
mods.manage=模组管理
|
||||
@@ -989,7 +986,6 @@ nbt.title=查看文件 - %s
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
datapack.add.title=选择要添加的数据包压缩包
|
||||
datapack.empty=没有数据包
|
||||
datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包
|
||||
datapack.title=世界 [%s] - 数据包
|
||||
|
||||
@@ -1008,7 +1004,6 @@ world.backup.create.new_one=创建新备份
|
||||
world.backup.create.failed=创建备份失败。\n%s
|
||||
world.backup.create.success=成功创建新备份:%s
|
||||
world.backup.delete=删除此备份
|
||||
world.backup.empty=没有备份
|
||||
world.backup.processing=正在备份中……
|
||||
world.chunkbase=世界地图
|
||||
world.chunkbase.end_city=末地城地图
|
||||
@@ -1028,7 +1023,6 @@ world.delete=删除此世界
|
||||
world.delete.failed=删除世界失败。\n%s
|
||||
world.download=下载世界
|
||||
world.download.title=世界下载 - %1s
|
||||
world.empty=没有世界
|
||||
world.export=导出此世界
|
||||
world.export.title=选择该世界的存储位置
|
||||
world.export.location=保存到
|
||||
@@ -1099,7 +1093,6 @@ resourcepack.add.title=选择要添加的资源包压缩包
|
||||
resourcepack.delete.failed=删除资源包失败
|
||||
resourcepack.download=下载资源包
|
||||
resourcepack.download.title=资源包下载 - %1s
|
||||
resourcepack.empty=没有资源包
|
||||
resourcepack.manage=资源包管理
|
||||
resourcepack.update_in_modpack.warning=更新整合包中的资源包可能导致整合包损坏,使游戏纹理和音效异常。你确定要更新吗?
|
||||
resourcepack.warning.invalid=资源包元数据无效
|
||||
@@ -1122,7 +1115,6 @@ schematics.create_directory.failed=创建文件夹失败
|
||||
schematics.create_directory.failed.already_exists=文件夹已存在
|
||||
schematics.create_directory.failed.empty_name=名称不能为空
|
||||
schematics.create_directory.failed.invalid_name=名称中包含非法字符
|
||||
schematics.empty=没有原理图
|
||||
schematics.info.description=描述
|
||||
schematics.info.enclosing_size=包围盒尺寸
|
||||
schematics.info.name=名称
|
||||
@@ -1144,7 +1136,6 @@ search.sort=排序
|
||||
search.first_page=第一页
|
||||
search.previous_page=上一页
|
||||
search.next_page=下一页
|
||||
search.no_results_found=搜索无结果
|
||||
search.last_page=最后一页
|
||||
search.page_n=%d / %s
|
||||
|
||||
|
||||
@@ -443,25 +443,25 @@
|
||||
"module": "javafx.base",
|
||||
"groupId": "org.glavo.hmcl.openjfx",
|
||||
"artifactId": "javafx-base",
|
||||
"version": "21.0.11-freebsd",
|
||||
"version": "14.0.2.1-freebsd",
|
||||
"classifier": "freebsd",
|
||||
"sha1": "91890866a767230184fe4f18ab9a19d19bed887d"
|
||||
"sha1": "7bac900f0ab0d4d6dcf178252cf37ee1e0470d40"
|
||||
},
|
||||
{
|
||||
"module": "javafx.graphics",
|
||||
"groupId": "org.glavo.hmcl.openjfx",
|
||||
"artifactId": "javafx-graphics",
|
||||
"version": "21.0.11-freebsd",
|
||||
"version": "14.0.2.1-freebsd",
|
||||
"classifier": "freebsd",
|
||||
"sha1": "bd9f01d923db548eb8ac7889d96036cd8297520f"
|
||||
"sha1": "7773ce02d1dc29160801e4e077ed2a26e93bed13"
|
||||
},
|
||||
{
|
||||
"module": "javafx.controls",
|
||||
"groupId": "org.glavo.hmcl.openjfx",
|
||||
"artifactId": "javafx-controls",
|
||||
"version": "21.0.11-freebsd",
|
||||
"version": "14.0.2.1-freebsd",
|
||||
"classifier": "freebsd",
|
||||
"sha1": "41590baecf9637e3fc1f7d9479527cf6eb9e8d44"
|
||||
"sha1": "34a3dd3ccbc898bacfdcb3256cfb87ddc50621d3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,42 +23,12 @@ import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/// Tests for detached launcher state migration.
|
||||
@NotNullByDefault
|
||||
public final class LauncherStateTest {
|
||||
/// Tests that settings listeners receive the observable field that changed.
|
||||
@Test
|
||||
public void reportsChangedFieldToListeners() {
|
||||
LauncherState state = new LauncherState();
|
||||
AtomicBoolean notified = new AtomicBoolean();
|
||||
state.addListener(source -> {
|
||||
assertSame(state.xProperty(), source);
|
||||
notified.set(true);
|
||||
});
|
||||
|
||||
state.setX(0.25);
|
||||
|
||||
assertTrue(notified.get());
|
||||
}
|
||||
|
||||
/// Tests that only window geometry fields defer automatic persistence.
|
||||
@Test
|
||||
public void identifiesDeferredWindowGeometryFields() {
|
||||
LauncherState state = new LauncherState();
|
||||
|
||||
assertFalse(state.shouldSaveImmediately(state.xProperty()));
|
||||
assertFalse(state.shouldSaveImmediately(state.yProperty()));
|
||||
assertFalse(state.shouldSaveImmediately(state.widthProperty()));
|
||||
assertFalse(state.shouldSaveImmediately(state.heightProperty()));
|
||||
assertTrue(state.shouldSaveImmediately(state.schemaProperty()));
|
||||
assertTrue(state.shouldSaveImmediately(state.promptedVersionProperty()));
|
||||
assertTrue(state.shouldSaveImmediately(state.getShownTips()));
|
||||
}
|
||||
|
||||
/// Tests extracting runtime state fields from a legacy config object.
|
||||
@Test
|
||||
public void extractsLauncherStateFromLegacyConfigJson() {
|
||||
|
||||
@@ -14,7 +14,6 @@ tasks.compileJava {
|
||||
dependencies {
|
||||
api(libs.kala.compress.zip)
|
||||
api(libs.kala.compress.tar)
|
||||
api(libs.kala.encoding.detctor)
|
||||
api(libs.simple.png.javafx)
|
||||
api(libs.gson)
|
||||
api(libs.tomlj)
|
||||
@@ -24,6 +23,7 @@ dependencies {
|
||||
api(libs.constant.pool.scanner)
|
||||
api(libs.nanohttpd)
|
||||
api(libs.jsoup)
|
||||
api(libs.chardet)
|
||||
api(libs.jna)
|
||||
api(libs.pci.ids)
|
||||
api(libs.hello.nbt)
|
||||
|
||||
-1
@@ -420,7 +420,6 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
loaders.stream().flatMap(loader -> {
|
||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
|
||||
else if ("legacy-fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LEGACY_FABRIC);
|
||||
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
|
||||
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
|
||||
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
|
||||
|
||||
+17
-34
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.addon.resourcepack;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import kala.encdet.EncodingDetector;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.meta.PackMcMeta;
|
||||
@@ -37,8 +36,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
@@ -227,46 +224,33 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
|
||||
this.optionsFile = repository.getRunDirectory(id).resolve("options.txt");
|
||||
}
|
||||
|
||||
private @Nullable Charset optionsFileEncoding;
|
||||
|
||||
@NotNull
|
||||
private Map<String, String> loadOptions() {
|
||||
getMinecraftVersion();
|
||||
Map<String, String> options = new LinkedHashMap<>();
|
||||
if (!Files.isRegularFile(optionsFile)) return options;
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = Files.readAllBytes(optionsFile);
|
||||
} catch (IOException | UncheckedIOException e) {
|
||||
LOG.warning("Failed to read instance options file", e);
|
||||
return options;
|
||||
}
|
||||
|
||||
EncodingDetector.Encoding bestEncoding = EncodingDetector.MODERN_WEB.detect(bytes).bestEncoding();
|
||||
if (bestEncoding == EncodingDetector.Encoding.ASCII)
|
||||
bestEncoding = EncodingDetector.Encoding.UTF_8;
|
||||
|
||||
optionsFileEncoding = bestEncoding != null && bestEncoding.approximateCharset() != null
|
||||
? bestEncoding.approximateCharset() : StandardCharsets.UTF_8;
|
||||
//noinspection DataFlowIssue
|
||||
new String(bytes, optionsFileEncoding).lines().forEach(s -> {
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
var entry = s.split(":", 2);
|
||||
if (entry.length == 2) {
|
||||
options.put(entry[0], entry[1]);
|
||||
try (var stream = Files.lines(optionsFile, StandardCharsets.UTF_8)) {
|
||||
stream.forEach(s -> {
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
var entry = s.split(":", 2);
|
||||
if (entry.length == 2) {
|
||||
options.put(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read instance options file", e);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private void saveOptions(@NotNull Map<String, String> options) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var entry : options.entrySet()) {
|
||||
sb.append(entry.getKey()).append(":").append(entry.getValue()).append(System.lineSeparator());
|
||||
}
|
||||
try {
|
||||
FileUtils.saveSafely(optionsFile, sb.toString(), optionsFileEncoding);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var entry : options.entrySet()) {
|
||||
sb.append(entry.getKey()).append(":").append(entry.getValue()).append(System.lineSeparator());
|
||||
}
|
||||
FileUtils.saveSafely(optionsFile, sb.toString());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save instance options file", e);
|
||||
}
|
||||
@@ -293,8 +277,7 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
|
||||
if (requiredVersion == null) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (requiredVersion == null)
|
||||
requiredVersion = getPackVersion(getMinecraftVersion(), repository.getVersionJar(id));
|
||||
if (requiredVersion == null) requiredVersion = getPackVersion(getMinecraftVersion(), repository.getVersionJar(id));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.jackhuang.hmcl.util.TypeUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
@@ -58,7 +57,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/// All subclasses of this class must call [#register()] once in their constructor.
|
||||
///
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public abstract class ObservableSetting implements Observable {
|
||||
|
||||
private static final ClassValue<List<? extends ObservableField<?>>> FIELDS = new ClassValue<>() {
|
||||
@@ -96,9 +94,6 @@ public abstract class ObservableSetting implements Observable {
|
||||
protected transient final Map<String, JsonElement> unknownFields = new HashMap<>();
|
||||
protected transient final DirtyTracker tracker = new DirtyTracker();
|
||||
|
||||
/// Whether a deferred field change still needs to be saved.
|
||||
private transient boolean savePending;
|
||||
|
||||
private boolean registered = false;
|
||||
|
||||
protected final void register() {
|
||||
@@ -116,26 +111,6 @@ public abstract class ObservableSetting implements Observable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a change to the given field should be saved immediately.
|
||||
///
|
||||
/// @param observable the changed observable field
|
||||
/// @return whether the change should trigger listeners immediately
|
||||
public boolean shouldSaveImmediately(Observable observable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns whether a deferred field change still needs to be saved.
|
||||
public final boolean isSavePending() {
|
||||
return savePending;
|
||||
}
|
||||
|
||||
/// Sets whether a deferred field change still needs to be saved.
|
||||
///
|
||||
/// @param savePending whether a deferred field change still needs to be saved
|
||||
public final void setSavePending(boolean savePending) {
|
||||
this.savePending = savePending;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
|
||||
@@ -19,16 +19,13 @@ package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import kala.compress.archivers.zip.ZipArchiveEntry;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import kala.compress.utils.BufferedDataInputSeekableChannel;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.ZipFileTree;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.spi.FileSystemProvider;
|
||||
@@ -131,18 +128,6 @@ public final class CompressingUtils {
|
||||
throw new IOException("Cannot find suitable encoding for the zip.");
|
||||
}
|
||||
|
||||
private static BufferedDataInputSeekableChannel openBufferedChannel(Path zipFile) throws IOException {
|
||||
SeekableByteChannel channel = Files.newByteChannel(zipFile);
|
||||
|
||||
try {
|
||||
return new BufferedDataInputSeekableChannel(channel, 8192, ByteOrder.LITTLE_ENDIAN);
|
||||
} catch (Throwable e) {
|
||||
IOUtils.closeQuietly(channel, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static ZipFileTree openZipTree(Path zipFile) throws IOException {
|
||||
return new ZipFileTree(openZipFile(zipFile));
|
||||
}
|
||||
@@ -152,14 +137,14 @@ public final class CompressingUtils {
|
||||
}
|
||||
|
||||
public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
|
||||
return new ZipArchiveReader(openBufferedChannel(zipFile), charset);
|
||||
return new ZipArchiveReader(zipFile, charset);
|
||||
}
|
||||
|
||||
public static ZipArchiveReader openZipFileWithPossibleEncoding(Path zipFile, Charset possibleEncoding) throws IOException {
|
||||
if (possibleEncoding == null)
|
||||
possibleEncoding = StandardCharsets.UTF_8;
|
||||
|
||||
ZipArchiveReader zipReader = new ZipArchiveReader(openBufferedChannel(zipFile));
|
||||
ZipArchiveReader zipReader = new ZipArchiveReader(Files.newByteChannel(zipFile));
|
||||
|
||||
Charset suitableEncoding;
|
||||
try {
|
||||
@@ -176,7 +161,7 @@ public final class CompressingUtils {
|
||||
}
|
||||
|
||||
zipReader.close();
|
||||
return new ZipArchiveReader(openBufferedChannel(zipFile), suitableEncoding);
|
||||
return new ZipArchiveReader(Files.newByteChannel(zipFile), suitableEncoding);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import org.glavo.chardet.DetectedCharset;
|
||||
import org.glavo.chardet.UniversalDetector;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -25,8 +27,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
@@ -38,6 +38,7 @@ import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -235,6 +236,24 @@ public final class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String readTextMaybeNativeEncoding(Path file) throws IOException {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
|
||||
if (OperatingSystem.NATIVE_CHARSET == UTF_8)
|
||||
return new String(bytes, UTF_8);
|
||||
|
||||
UniversalDetector detector = new UniversalDetector();
|
||||
detector.handleData(bytes);
|
||||
detector.dataEnd();
|
||||
|
||||
DetectedCharset detectedCharset = detector.getDetectedCharset();
|
||||
if (detectedCharset != null && detectedCharset.isSupported()
|
||||
&& (detectedCharset == DetectedCharset.UTF_8 || detectedCharset == DetectedCharset.US_ASCII))
|
||||
return new String(bytes, UTF_8);
|
||||
else
|
||||
return new String(bytes, OperatingSystem.NATIVE_CHARSET);
|
||||
}
|
||||
|
||||
public static void deleteDirectory(Path directory) throws IOException {
|
||||
if (!Files.exists(directory))
|
||||
return;
|
||||
@@ -488,17 +507,13 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static void saveSafely(Path file, String content) throws IOException {
|
||||
saveSafely(file, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void saveSafely(Path file, String content, @Nullable Charset charset) throws IOException {
|
||||
Path parent = file.toAbsolutePath().getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
|
||||
Path tmpFile = tmpSaveFile(file);
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, charset != null ? charset : StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
|
||||
writer.write(content);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.glavo.chardet.DetectedCharset;
|
||||
import org.glavo.chardet.UniversalDetector;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
import static org.jackhuang.hmcl.util.platform.OperatingSystem.NATIVE_CHARSET;
|
||||
|
||||
/**
|
||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
||||
@@ -35,6 +43,25 @@ public final class IOUtils {
|
||||
|
||||
public static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
public static BufferedReader newBufferedReaderMaybeNativeEncoding(Path file) throws IOException {
|
||||
if (NATIVE_CHARSET == UTF_8)
|
||||
return Files.newBufferedReader(file);
|
||||
|
||||
FileChannel channel = FileChannel.open(file);
|
||||
try {
|
||||
long oldPosition = channel.position();
|
||||
DetectedCharset detectedCharset = UniversalDetector.detectCharset(channel);
|
||||
Charset charset = detectedCharset != null && detectedCharset.isSupported()
|
||||
&& (detectedCharset.getCharset() == UTF_8 || detectedCharset.getCharset() == US_ASCII)
|
||||
? UTF_8 : NATIVE_CHARSET;
|
||||
channel.position(oldPosition);
|
||||
return new BufferedReader(new InputStreamReader(Channels.newInputStream(channel), charset));
|
||||
} catch (Throwable e) {
|
||||
closeQuietly(channel, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream stream) throws IOException {
|
||||
try (stream) {
|
||||
return stream.readAllBytes();
|
||||
|
||||
@@ -394,7 +394,7 @@ public final class NetworkUtils {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (params != null) {
|
||||
for (Map.Entry<String, String> e : params.entrySet())
|
||||
sb.append(encodeURL(e.getKey())).append("=").append(encodeURL(e.getValue())).append("&");
|
||||
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
}
|
||||
return doPost(u, sb.toString());
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -29,7 +28,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public final class ObservableHelper implements Observable, InvalidationListener {
|
||||
|
||||
private final List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
|
||||
@@ -65,7 +63,7 @@ public final class ObservableHelper implements Observable, InvalidationListener
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
listeners.forEach(it -> it.invalidated(observable));
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
public void receiveUpdatesFrom(Observable observable) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform.windows;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Provides mappings for the GDI functions used by HMCL.
|
||||
///
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public interface Gdi32 extends StdCallLibrary {
|
||||
|
||||
/// The loaded GDI library, or `null` when JNA is unavailable or the current platform is not Windows.
|
||||
@Nullable Gdi32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
|
||||
? NativeUtils.load("gdi32", Gdi32.class)
|
||||
: null;
|
||||
|
||||
/// The [GetDeviceCaps](https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps)
|
||||
/// index for the current vertical refresh rate, in hertz.
|
||||
int VREFRESH = 116;
|
||||
|
||||
/// Retrieves device-specific information for the supplied device context.
|
||||
///
|
||||
/// @param hdc the device context to query
|
||||
/// @param index the capability index to retrieve
|
||||
/// @return the value of the requested capability
|
||||
/// @see <a href="https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps">GetDeviceCaps function</a>
|
||||
int GetDeviceCaps(Pointer hdc, int index);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform.windows;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Provides mappings for the user interface functions used by HMCL.
|
||||
///
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public interface User32 extends StdCallLibrary {
|
||||
|
||||
/// The loaded User32 library, or `null` when JNA is unavailable or the current platform is not Windows.
|
||||
@Nullable User32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
|
||||
? NativeUtils.load("user32", User32.class)
|
||||
: null;
|
||||
|
||||
/// Retrieves a device context for a window or for the entire screen.
|
||||
///
|
||||
/// @param hWnd the target window, or `null` to retrieve the screen device context
|
||||
/// @return the device context, or `null` if the function fails
|
||||
/// @see <a href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getdc">GetDC function</a>
|
||||
@Nullable Pointer GetDC(@Nullable Pointer hWnd);
|
||||
|
||||
/// Releases a device context retrieved by {@link #GetDC(Pointer)}.
|
||||
///
|
||||
/// @param hWnd the same window passed to {@link #GetDC(Pointer)}, or `null` for a screen device context
|
||||
/// @param hDC the device context to release
|
||||
/// @return `1` if the device context was released, or `0` if it was not released
|
||||
/// @see <a href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-releasedc">ReleaseDC function</a>
|
||||
int ReleaseDC(@Nullable Pointer hWnd, Pointer hDC);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public final class JavaFXPlatform {
|
||||
ALL.put("linux-riscv64", new JavaFXPlatform("linux", GLAVO_GROUP_ID, "19.0.2.1-riscv64"));
|
||||
|
||||
// FreeBSD
|
||||
ALL.put("freebsd-x86_64", new JavaFXPlatform("freebsd", GLAVO_GROUP_ID, "21.0.11-freebsd"));
|
||||
ALL.put("freebsd-x86_64", new JavaFXPlatform("freebsd", GLAVO_GROUP_ID, "14.0.2.1-freebsd"));
|
||||
}
|
||||
|
||||
private final String classifier;
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
versionRoot=3.17
|
||||
versionRoot=3.16
|
||||
@@ -2,7 +2,6 @@
|
||||
hmclauncher = "3.7.0.1"
|
||||
jetbrains-annotations = "26.1.0"
|
||||
kala-compress = "1.27.1-3"
|
||||
kala-encoding-detctor = "0.1.0"
|
||||
simple-png-javafx = "0.3.0"
|
||||
gson = "2.13.2"
|
||||
tomlj = "1.1.1"
|
||||
@@ -13,6 +12,7 @@ constant-pool-scanner = "1.2"
|
||||
hello-nbt = "0.3.0"
|
||||
nanohttpd = "2.3.1"
|
||||
jsoup = "1.21.2"
|
||||
chardet = "2.5.0"
|
||||
fxsvgimage = "1.3"
|
||||
jna = "5.18.1"
|
||||
pci-ids = "0.4.0"
|
||||
@@ -39,7 +39,6 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j
|
||||
kala-compress-zip = { module = "org.glavo.kala:kala-compress-archivers-zip", version.ref = "kala-compress" }
|
||||
kala-compress-tar = { module = "org.glavo.kala:kala-compress-archivers-tar", version.ref = "kala-compress" }
|
||||
kala-compress-ar = { module = "org.glavo.kala:kala-compress-archivers-ar", version.ref = "kala-compress" }
|
||||
kala-encoding-detctor = { module = "org.glavo:kala-encoding-detector", version.ref = "kala-encoding-detctor" }
|
||||
simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "simple-png-javafx" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
|
||||
@@ -50,6 +49,7 @@ constant-pool-scanner = { module = "org.jenkins-ci:constant-pool-scanner", versi
|
||||
hello-nbt = { module = "org.glavo:HelloNBT", version.ref = "hello-nbt" }
|
||||
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
|
||||
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
|
||||
|
||||
Reference in new issue
Block a user