Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf21cf4a3c | ||
|
|
a0d1fa8f38 | ||
|
|
78b8d37891 | ||
|
|
0807c04572 | ||
|
|
8eec18450e | ||
|
|
15d4874366 | ||
|
|
c149401cf3 | ||
|
|
ff459a5bc4 | ||
|
|
5796dad18c | ||
|
|
0443ff7cd1 | ||
|
|
4c6fe865f5 | ||
|
|
1e54482f3b | ||
|
|
c329f0d2e8 | ||
|
|
af4d9feea5 | ||
|
|
af71c71663 | ||
|
|
4675442ecd | ||
|
|
c8929c80d9 | ||
|
|
81634b14df | ||
|
|
ccca396d91 | ||
|
|
5a14449b44 | ||
|
|
612998a98f | ||
|
|
a50e327f70 | ||
|
|
7aeb1642b1 | ||
|
|
5fc1536d6a | ||
|
|
463cf2ff0f | ||
|
|
144e822a85 | ||
|
|
87789beeb7 | ||
|
|
bb86100c54 | ||
|
|
2ceca9d8cc | ||
|
|
d5fc365a96 | ||
|
|
d5c86a8d88 | ||
|
|
bd6beb1369 | ||
|
|
b1719e77bb | ||
|
|
77f0a90f10 | ||
|
|
ff6110ba89 | ||
|
|
e7e964b3dd | ||
|
|
4e29c29a7a | ||
|
|
25ad1dbf44 | ||
|
|
d2e7e0d626 | ||
|
|
c68bbf2904 | ||
|
|
d0fc46f930 | ||
|
|
f1b861eb21 | ||
|
|
5cde4ce757 | ||
|
|
d297417909 | ||
|
|
2854eac27d | ||
|
|
c9fd7c6a1c | ||
|
|
0e0517ab3c | ||
|
|
7a4bea10d0 | ||
|
|
b87d40ffcc | ||
|
|
1277bf8b5d | ||
|
|
c2e807055a | ||
|
|
9021ec1960 | ||
|
|
82a47e9e2d | ||
|
|
4841204fa0 | ||
|
|
897f5fe11b | ||
|
|
4657e3273e | ||
|
|
4edea5f5b9 | ||
|
|
36bd7d4491 | ||
|
|
3aa8603bd1 | ||
|
|
ca7a61e20a | ||
|
|
21afc81cf8 | ||
|
|
94a0e26be5 | ||
|
|
14e8d2d0b5 | ||
|
|
49c408cb23 | ||
|
|
b81c3398ac | ||
|
|
2855df3693 | ||
|
|
8184dc6cee | ||
|
|
df21559c0e | ||
|
|
73ff3081f8 | ||
|
|
fe05bb58a4 | ||
|
|
4c9cd950e4 | ||
|
|
646d9a09a5 | ||
|
|
255fe75640 | ||
|
|
de15dd8e98 | ||
|
|
886319a6c7 | ||
|
|
3320f6c341 | ||
|
|
b85be78166 | ||
|
|
a34a7439d7 | ||
|
|
591fd51600 | ||
|
|
c1407a9620 | ||
|
|
e0a91579f2 | ||
|
|
b68f404c3c | ||
|
|
c1f50f9ba3 | ||
|
|
d8bb2131f2 | ||
|
|
aca761385b | ||
|
|
15faccf376 | ||
|
|
241c9c419c | ||
|
|
4b36f8ce03 | ||
|
|
5c0baad782 | ||
|
|
850a56405c | ||
|
|
e8487c467b | ||
|
|
9866d88a16 | ||
|
|
af2d46ae89 | ||
|
|
6f7c561ecb | ||
|
|
93abe31acf | ||
|
|
a6f8659349 | ||
|
|
8728c89093 |
No files matched your search
@@ -1,3 +0,0 @@
|
||||
code_review:
|
||||
pull_request_opened:
|
||||
code_review: false
|
||||
@@ -0,0 +1,105 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,9 @@ jobs:
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon --parallel
|
||||
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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,11 @@ These rules apply to all Java code written or modified in this repository.
|
||||
|
||||
## Nullability
|
||||
|
||||
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
|
||||
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
|
||||
- Nullability must never be implicit.
|
||||
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
|
||||
|
||||
- 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.
|
||||
|
||||
## Immutability
|
||||
|
||||
@@ -15,6 +17,8 @@ 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 by configurations.registering
|
||||
val embedResources = configurations.register("embedResources")
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
@@ -166,7 +166,7 @@ val hmclProperties = buildList {
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile by tasks.registering {
|
||||
val createPropertiesFile = tasks.register("createPropertiesFile") {
|
||||
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 by tasks.registering {
|
||||
val makeExecutables = tasks.register("makeExecutables") {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
dependsOn(tasks.jar)
|
||||
@@ -286,7 +286,7 @@ val makeExecutables by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
val makeDeb by tasks.registering(CreateDeb::class) {
|
||||
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
|
||||
dependsOn(makeExecutables)
|
||||
|
||||
val debFile = layout.file(provider { artifactFile("deb") })
|
||||
@@ -299,6 +299,7 @@ val makeDeb by tasks.registering(CreateDeb::class) {
|
||||
|
||||
version.set(project.version.toString())
|
||||
releaseType.set(debChannel)
|
||||
launcherClassName.set("org.jackhuang.hmcl.Launcher")
|
||||
appShFile.set(layout.file(provider { artifactFile("sh") }))
|
||||
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
|
||||
outputFile.set(debFile)
|
||||
@@ -438,19 +439,19 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
|
||||
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 by tasks.registering(CreateLanguageList::class) {
|
||||
val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList") {
|
||||
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 by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||
|
||||
@@ -12,31 +12,20 @@ import com.jfoenix.controls.JFXRippler.RipplerPos;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import com.jfoenix.transitions.JFXFillTransition;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.skin.CheckBoxSkin;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
private final StackPane box = new StackPane();
|
||||
@@ -186,15 +175,32 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
}
|
||||
|
||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||
this.transition.setRate(selection ? 1.0 : -1.0);
|
||||
this.select.setRate(selection ? 1.0 : -1.0);
|
||||
this.transition.play();
|
||||
this.select.play();
|
||||
|
||||
this.box.setBorder(new Border(new BorderStroke(
|
||||
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
||||
BorderStrokeStyle.SOLID,
|
||||
new CornerRadii(2.0),
|
||||
new BorderWidths(this.lineThick))));
|
||||
|
||||
if (!AnimationUtils.isAnimationEnabled()) {
|
||||
if (selection) {
|
||||
this.mark.setVisible(true);
|
||||
this.mark.setScaleX(1.0);
|
||||
this.mark.setScaleY(1.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, control.getCheckedColor());
|
||||
} else {
|
||||
this.mark.setVisible(false);
|
||||
this.mark.setScaleX(0.0);
|
||||
this.mark.setScaleY(0.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, Color.TRANSPARENT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.transition.setRate(selection ? 1.0 : -1.0);
|
||||
this.select.setRate(selection ? 1.0 : -1.0);
|
||||
this.transition.play();
|
||||
this.select.play();
|
||||
}
|
||||
|
||||
private void createFillTransition() {
|
||||
|
||||
@@ -52,8 +52,6 @@ public final class EntryPoint {
|
||||
|
||||
checkWine();
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
||||
if (!isInsideMacAppBundle())
|
||||
@@ -74,71 +72,6 @@ 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 {
|
||||
@@ -225,15 +158,8 @@ public final class EntryPoint {
|
||||
|
||||
private static void checkWine() {
|
||||
if (OperatingSystem.isRunningUnderWine()) {
|
||||
SwingUtils.initLookAndFeel();
|
||||
LOG.warning("HMCL is running under Wine or its distributions!");
|
||||
|
||||
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
|
||||
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
|
||||
exit(1);
|
||||
}
|
||||
showWarning(i18n("fatal.wine_warning"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,10 +199,21 @@ 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.
|
||||
*/
|
||||
private static void showErrorAndExit(String message) {
|
||||
static void showErrorAndExit(String message) {
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.WString;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -29,8 +31,6 @@ 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;
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
@@ -48,7 +49,14 @@ 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.Shell32;
|
||||
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;
|
||||
@@ -60,7 +68,6 @@ 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;
|
||||
@@ -98,18 +105,7 @@ public final class Launcher extends Application {
|
||||
}
|
||||
|
||||
try {
|
||||
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);
|
||||
}
|
||||
initializeSettingsRuntime();
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
@@ -146,6 +142,7 @@ public final class Launcher extends Application {
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(primaryStage);
|
||||
primaryStage.show();
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
@@ -274,52 +271,10 @@ 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();
|
||||
FileSaver.shutdown();
|
||||
SettingsManager.shutdown();
|
||||
LOG.shutdown();
|
||||
}
|
||||
|
||||
@@ -376,6 +331,20 @@ 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();
|
||||
setupWindowsAppUserModelID();
|
||||
|
||||
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);
|
||||
@@ -408,5 +377,177 @@ public final class Launcher extends Application {
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets the process-level AppUserModelID on Windows so the launcher groups correctly on the taskbar.
|
||||
private static void setupWindowsAppUserModelID() {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
||||
return;
|
||||
if (!NativeUtils.USE_JNA || Shell32.INSTANCE == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
int hr = Shell32.INSTANCE.SetCurrentProcessExplicitAppUserModelID(new WString(Metadata.WINDOWS_APP_USER_MODEL_ID));
|
||||
if (hr < 0) {
|
||||
LOG.warning("Failed to set AppUserModelID, HRESULT=0x" + Integer.toHexString(hr));
|
||||
} else {
|
||||
LOG.info("Set AppUserModelID: " + Metadata.WINDOWS_APP_USER_MODEL_ID);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set AppUserModelID", e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -37,6 +37,9 @@ public final class Metadata {
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
||||
|
||||
/// Explicit Application User Model ID used for Windows taskbar grouping and pinning.
|
||||
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
|
||||
@@ -42,16 +42,16 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, version, authInfo, options, null);
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, manifest, authInfo, options, null);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, version, authInfo, options, listener, true);
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, manifest, authInfo, options, listener, true);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, version, authInfo, options, listener, daemon);
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, manifest, authInfo, options, listener, daemon);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,7 +66,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
if (options.isDisableAutoGameOptions())
|
||||
return;
|
||||
|
||||
Path runDir = repository.getRunDirectory(version.getId());
|
||||
Path runDir = repository.getRunDirectory(manifest.id());
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
@@ -91,7 +91,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(manifest));
|
||||
if (gameVersion.compareTo("1.1") < 0)
|
||||
return;
|
||||
|
||||
@@ -161,7 +161,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
if (options.isAllowAutoAgent()
|
||||
&& !options.isNoGeneratedJVMArgs()
|
||||
&& !options.isNoGeneratedOptimizingJVMArgs()
|
||||
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
|
||||
&& NativePatcher.needPatchMemoryUtil(manifest, options.getJava().getParsedVersion())) {
|
||||
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
|
||||
try {
|
||||
result.add("-javaagent:" + extractLwjglUnsafeAgent());
|
||||
@@ -178,9 +178,9 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
|
||||
String fileName = library.getArtifact().getFileName();
|
||||
String fileName = library.artifact().getFileName();
|
||||
|
||||
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
|
||||
Path agentPath = repository.getLibraryFile(manifest, library).toAbsolutePath().normalize();
|
||||
if (agentPath.toString().contains("=")) {
|
||||
throw new IOException("Invalid library path: " + agentPath);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
@@ -45,7 +46,7 @@ import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.setting.ProxyType;
|
||||
import org.jackhuang.hmcl.setting.SettingFileUtils;
|
||||
import org.jackhuang.hmcl.setting.GameSettingsPresetID;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -53,7 +54,9 @@ import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Bits;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
@@ -81,10 +84,10 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
/// @param repository the owning game repository
|
||||
/// @param instanceId the game instance ID, or `null` when only repository context is available
|
||||
@NotNullByDefault
|
||||
public record InstanceReference(HMCLGameRepository repository, @Nullable String instanceId) {
|
||||
public record InstanceReference(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
}
|
||||
|
||||
/// Directory under the version root that stores HMCL-managed instance metadata.
|
||||
/// Directory under the instance root that stores HMCL-managed instance metadata.
|
||||
private static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance configuration files.
|
||||
@@ -100,22 +103,22 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final GameDirectory gameDirectory;
|
||||
|
||||
/// The selected instance ID persisted for this repository's game directory.
|
||||
private final StringBinding selectedInstance;
|
||||
private final ObjectBinding<@Nullable GameInstanceID> selectedInstance;
|
||||
|
||||
// instance game settings
|
||||
private final Map<String, GameSettings.Instance> instanceGameSettings = new HashMap<>();
|
||||
private final Map<GameInstanceID, GameSettings.Instance> instanceGameSettings = new HashMap<>();
|
||||
/// Instance IDs whose local game settings file has already been checked.
|
||||
private final Set<String> loadedInstanceGameSettings = new HashSet<>();
|
||||
private final Set<String> readOnlyInstanceGameSettings = new HashSet<>();
|
||||
private final Set<String> beingModpackVersions = new HashSet<>();
|
||||
private final Set<GameInstanceID> loadedInstanceGameSettings = new HashSet<>();
|
||||
private final Set<GameInstanceID> readOnlyInstanceGameSettings = new HashSet<>();
|
||||
private final Set<GameInstanceID> beingModpackInstances = new HashSet<>();
|
||||
|
||||
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
|
||||
public final EventManager<Event> onInstanceIconChanged = new EventManager<>();
|
||||
|
||||
/// Creates a repository backed by the given game directory.
|
||||
public HMCLGameRepository(GameDirectory gameDirectory) {
|
||||
super(gameDirectory.getPath().toPath());
|
||||
this.gameDirectory = gameDirectory;
|
||||
this.selectedInstance = Bindings.stringValueAt(settings().getSelectedInstance(), gameDirectory.getId());
|
||||
this.selectedInstance = Bindings.valueAt(settings().getSelectedInstance(), gameDirectory.getId());
|
||||
gameDirectory.pathProperty().addListener((a, b, newValue) -> changeDirectory(newValue.toPath()));
|
||||
}
|
||||
|
||||
@@ -125,26 +128,26 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
/// Returns the selected instance ID property for this repository's game directory.
|
||||
public StringBinding selectedInstanceProperty() {
|
||||
public Binding<@Nullable GameInstanceID> selectedInstanceProperty() {
|
||||
return selectedInstance;
|
||||
}
|
||||
|
||||
/// Returns the selected instance ID for this repository's game directory.
|
||||
public @Nullable String getSelectedInstance() {
|
||||
public @Nullable GameInstanceID getSelectedInstance() {
|
||||
return selectedInstance.get();
|
||||
}
|
||||
|
||||
/// Sets the selected instance ID for this repository's game directory.
|
||||
public void setSelectedInstance(@Nullable String instance) {
|
||||
settings().setSelectedInstance(gameDirectory.getId(), instance);
|
||||
public void setSelectedInstance(@Nullable GameInstanceID instanceId) {
|
||||
settings().setSelectedInstance(gameDirectory.getId(), instanceId);
|
||||
}
|
||||
|
||||
/// Refreshes the selected instance ID after versions are loaded.
|
||||
/// Refreshes the selected instance ID after instances are loaded.
|
||||
public void refreshSelectedInstance() {
|
||||
@Nullable String selectedInstance = settings().getSelectedInstance(gameDirectory.getId());
|
||||
@Nullable String refreshedInstance = selectedInstance;
|
||||
if (!hasVersion(refreshedInstance)) {
|
||||
refreshedInstance = getVersions().isEmpty() ? null : getVersions().iterator().next().getId();
|
||||
@Nullable GameInstanceID selectedInstance = settings().getSelectedInstance(gameDirectory.getId());
|
||||
@Nullable GameInstanceID refreshedInstance = selectedInstance;
|
||||
if (refreshedInstance == null || !hasInstance(refreshedInstance)) {
|
||||
refreshedInstance = getInstanceManifests().isEmpty() ? null : getInstanceManifests().iterator().next().id();
|
||||
}
|
||||
if (!Objects.equals(selectedInstance, refreshedInstance)) {
|
||||
setSelectedInstance(refreshedInstance);
|
||||
@@ -162,24 +165,24 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getRunDirectory(String id) {
|
||||
if (beingModpackVersions.contains(id) || isModpack(id)) {
|
||||
return getVersionRoot(id);
|
||||
public Path getRunDirectory(GameInstanceID instanceId) {
|
||||
if (beingModpackInstances.contains(instanceId) || isModpack(instanceId)) {
|
||||
return getInstanceRoot(instanceId);
|
||||
}
|
||||
|
||||
GameSettings.Instance localSetting = getInstanceGameSettings(id);
|
||||
GameSettings.Instance localSetting = getInstanceGameSettings(instanceId);
|
||||
boolean useInstanceRunningDirectory =
|
||||
localSetting != null && localSetting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
|
||||
String runningDirectory = getSelectedRunningDirectory(localSetting, useInstanceRunningDirectory);
|
||||
if (StringUtils.isBlank(runningDirectory)) {
|
||||
return useInstanceRunningDirectory ? getVersionRoot(id) : super.getRunDirectory(id);
|
||||
return useInstanceRunningDirectory ? getInstanceRoot(instanceId) : super.getRunDirectory(instanceId);
|
||||
}
|
||||
|
||||
try {
|
||||
return Path.of(runningDirectory);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return getVersionRoot(id);
|
||||
return getInstanceRoot(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,24 +204,24 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return Objects.requireNonNullElse(parent.runningDirectoryProperty().getValue(), "");
|
||||
}
|
||||
|
||||
public Stream<Version> getDisplayVersions() {
|
||||
return getVersions().stream()
|
||||
public Stream<GameInstanceManifest> getDisplayInstanceManifests() {
|
||||
return getInstanceManifests().stream()
|
||||
.filter(v -> !v.isHidden())
|
||||
.sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH))
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
|
||||
.sorted(Comparator.comparing((GameInstanceManifest v) -> Lang.requireNonNullElse(v.releaseTime(), Instant.EPOCH))
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.id().id())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshVersionsImpl() {
|
||||
protected void refreshImpl() {
|
||||
instanceGameSettings.clear();
|
||||
loadedInstanceGameSettings.clear();
|
||||
readOnlyInstanceGameSettings.clear();
|
||||
super.refreshVersionsImpl();
|
||||
versions.keySet().forEach(this::loadInstanceGameSettings);
|
||||
super.refreshImpl();
|
||||
getInstanceManifests().stream().map(GameInstanceManifest::id).forEach(this::loadInstanceGameSettings);
|
||||
|
||||
try {
|
||||
Path file = getBaseDirectory().resolve("launcher_profiles.json");
|
||||
if (!Files.exists(file) && !versions.isEmpty()) {
|
||||
if (!Files.exists(file) && !getInstanceManifests().isEmpty()) {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, PROFILE);
|
||||
}
|
||||
@@ -229,7 +232,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public void changeDirectory(Path newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersionsAsync().start();
|
||||
refreshAsync().start();
|
||||
}
|
||||
|
||||
private void clean(Path directory) throws IOException {
|
||||
@@ -237,56 +240,56 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
FileUtils.deleteDirectory(directory.resolve("logs"));
|
||||
}
|
||||
|
||||
public void clean(String id) throws IOException {
|
||||
public void clean(GameInstanceID instanceId) throws IOException {
|
||||
clean(getBaseDirectory());
|
||||
clean(getRunDirectory(id));
|
||||
clean(getRunDirectory(instanceId));
|
||||
}
|
||||
|
||||
/// Removes a version from disk and drops any cached instance settings for that version.
|
||||
/// Removes an instance from disk and clears its cached HMCL settings state.
|
||||
@Override
|
||||
public boolean removeVersionFromDisk(String id) {
|
||||
boolean removed = super.removeVersionFromDisk(id);
|
||||
public boolean removeInstanceFromDisk(GameInstanceID instanceId) {
|
||||
boolean removed = super.removeInstanceFromDisk(instanceId);
|
||||
if (removed) {
|
||||
instanceGameSettings.remove(id);
|
||||
loadedInstanceGameSettings.remove(id);
|
||||
readOnlyInstanceGameSettings.remove(id);
|
||||
beingModpackVersions.remove(id);
|
||||
instanceGameSettings.remove(instanceId);
|
||||
loadedInstanceGameSettings.remove(instanceId);
|
||||
readOnlyInstanceGameSettings.remove(instanceId);
|
||||
beingModpackInstances.remove(instanceId);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
Path srcDir = getVersionRoot(srcId);
|
||||
Path dstDir = getVersionRoot(dstId);
|
||||
public void duplicateInstance(GameInstanceID srcId, GameInstanceID dstId, boolean copySaves) throws IOException {
|
||||
Path srcDir = getInstanceRoot(srcId);
|
||||
Path dstDir = getInstanceRoot(dstId);
|
||||
|
||||
Version fromVersion = getVersion(srcId);
|
||||
GameInstanceManifest fromManifest = getInstanceManifest(srcId);
|
||||
|
||||
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
blackList.add(srcId.id() + ".jar");
|
||||
blackList.add(srcId.id() + ".json");
|
||||
if (!copySaves)
|
||||
blackList.add("saves");
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
if (Files.exists(dstDir)) throw new IOException("Instance exists");
|
||||
|
||||
Files.createDirectories(dstDir);
|
||||
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
|
||||
Path fromJson = srcDir.resolve(srcId + ".json");
|
||||
Path fromJar = srcDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
Path fromJson = srcDir.resolve(srcId.id() + ".json");
|
||||
Path fromJar = srcDir.resolve(srcId.id() + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId.id() + ".json");
|
||||
Path toJar = dstDir.resolve(dstId.id() + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.copy(fromJar, toJar);
|
||||
}
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId).setJar(dstId));
|
||||
JsonUtils.writeToJsonFile(toJson, fromManifest.withId(dstId).withJar(dstId));
|
||||
|
||||
boolean copyOriginalGameDir;
|
||||
try {
|
||||
copyOriginalGameDir = !Files.isSameFile(getRunDirectory(srcId), getVersionRoot(srcId));
|
||||
copyOriginalGameDir = !Files.isSameFile(getRunDirectory(srcId), getInstanceRoot(srcId));
|
||||
} catch (IOException e) {
|
||||
copyOriginalGameDir = true;
|
||||
}
|
||||
@@ -305,48 +308,48 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
}
|
||||
|
||||
private GameSettings.Instance copyInstanceGameSettings(String id) {
|
||||
GameSettings.Instance setting = getInstanceGameSettings(id);
|
||||
private GameSettings.Instance copyInstanceGameSettings(GameInstanceID instanceId) {
|
||||
GameSettings.Instance setting = getInstanceGameSettings(instanceId);
|
||||
if (setting != null) {
|
||||
return JsonUtils.clone(LauncherSettings.SETTINGS_GSON, setting, TypeToken.get(GameSettings.Instance.class));
|
||||
}
|
||||
|
||||
GameSettings.Instance copied = new GameSettings.Instance();
|
||||
copied.parentProperty().setValue(getEffectiveGameSettings(id).getPreset().idProperty().getValue());
|
||||
copied.parentProperty().setValue(getEffectiveGameSettings(instanceId).getPreset().idProperty().getValue());
|
||||
return copied;
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed metadata directory under the version root.
|
||||
/// Returns the HMCL-managed metadata directory under the instance root.
|
||||
///
|
||||
/// This directory stores instance-scoped files owned by HMCL.
|
||||
public Path getInstanceMetadataDirectory(String id) {
|
||||
return getVersionRoot(id).resolve(INSTANCE_METADATA_DIRECTORY);
|
||||
public Path getInstanceMetadataDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceRoot(instanceId).resolve(INSTANCE_METADATA_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed configuration directory under the instance metadata directory.
|
||||
public Path getInstanceConfigDirectory(String id) {
|
||||
return getInstanceMetadataDirectory(id).resolve(INSTANCE_CONFIG_DIRECTORY);
|
||||
public Path getInstanceConfigDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_CONFIG_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed state directory under the instance metadata directory.
|
||||
public Path getInstanceStateDirectory(String id) {
|
||||
return getInstanceMetadataDirectory(id).resolve(INSTANCE_STATE_DIRECTORY);
|
||||
public Path getInstanceStateDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_STATE_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the current local game settings path under the instance configuration directory.
|
||||
private Path getInstanceGameSettingsFile(String id) {
|
||||
return getInstanceConfigDirectory(id).resolve(INSTANCE_GAME_SETTINGS_FILENAME);
|
||||
private Path getInstanceGameSettingsFile(GameInstanceID instanceId) {
|
||||
return getInstanceConfigDirectory(instanceId).resolve(INSTANCE_GAME_SETTINGS_FILENAME);
|
||||
}
|
||||
|
||||
private void loadInstanceGameSettings(String id) {
|
||||
loadedInstanceGameSettings.add(id);
|
||||
InstanceGameSettingsLoadResult result = loadGameSettingsFile(getInstanceGameSettingsFile(id));
|
||||
private void loadInstanceGameSettings(GameInstanceID instanceId) {
|
||||
loadedInstanceGameSettings.add(instanceId);
|
||||
InstanceGameSettingsLoadResult result = loadGameSettingsFile(getInstanceGameSettingsFile(instanceId));
|
||||
if (result.setting() != null) {
|
||||
initInstanceGameSettings(id, result.setting(), result.allowSave());
|
||||
initInstanceGameSettings(instanceId, result.setting(), result.allowSave());
|
||||
return;
|
||||
}
|
||||
if (!result.allowSave()) {
|
||||
readOnlyInstanceGameSettings.add(id);
|
||||
readOnlyInstanceGameSettings.add(instanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,15 +360,15 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
|
||||
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
|
||||
this, id,
|
||||
this, instanceId,
|
||||
legacyParent);
|
||||
if (migrationResult != null) {
|
||||
initInstanceGameSettings(id, migrationResult.setting());
|
||||
initInstanceGameSettings(instanceId, migrationResult.setting());
|
||||
try {
|
||||
saveGameSettingsSync(id);
|
||||
saveGameSettingsSync(instanceId);
|
||||
migrationResult.saveReceipt();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save migrated instance game settings for " + id, e);
|
||||
LOG.warning("Failed to save migrated instance game settings for " + instanceId, e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -430,35 +433,35 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable GameSettings.Instance createInstanceGameSettings(String id) {
|
||||
if (!hasVersion(id)) {
|
||||
public @Nullable GameSettings.Instance createInstanceGameSettings(GameInstanceID instanceId) {
|
||||
if (!hasInstance(instanceId)) {
|
||||
return null;
|
||||
}
|
||||
if (readOnlyInstanceGameSettings.contains(id)) {
|
||||
if (readOnlyInstanceGameSettings.contains(instanceId)) {
|
||||
return null;
|
||||
}
|
||||
if (instanceGameSettings.containsKey(id)) {
|
||||
return getInstanceGameSettings(id);
|
||||
if (instanceGameSettings.containsKey(instanceId)) {
|
||||
return getInstanceGameSettings(instanceId);
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = new GameSettings.Instance();
|
||||
return initInstanceGameSettings(id, setting);
|
||||
return initInstanceGameSettings(instanceId, setting);
|
||||
}
|
||||
|
||||
private GameSettings.Instance initInstanceGameSettings(String id, GameSettings.Instance setting) {
|
||||
return initInstanceGameSettings(id, setting, true);
|
||||
private GameSettings.Instance initInstanceGameSettings(GameInstanceID instanceId, GameSettings.Instance setting) {
|
||||
return initInstanceGameSettings(instanceId, setting, true);
|
||||
}
|
||||
|
||||
private GameSettings.Instance initInstanceGameSettings(String id, GameSettings.Instance setting, boolean allowSave) {
|
||||
private GameSettings.Instance initInstanceGameSettings(GameInstanceID instanceId, GameSettings.Instance setting, boolean allowSave) {
|
||||
normalizeRunningDirectoryOverride(setting);
|
||||
setting.setSavable(allowSave);
|
||||
loadedInstanceGameSettings.add(id);
|
||||
instanceGameSettings.put(id, setting);
|
||||
loadedInstanceGameSettings.add(instanceId);
|
||||
instanceGameSettings.put(instanceId, setting);
|
||||
if (allowSave) {
|
||||
readOnlyInstanceGameSettings.remove(id);
|
||||
setting.addListener(a -> saveGameSettings(id));
|
||||
readOnlyInstanceGameSettings.remove(instanceId);
|
||||
setting.addListener(a -> saveGameSettings(instanceId));
|
||||
} else {
|
||||
readOnlyInstanceGameSettings.add(id);
|
||||
readOnlyInstanceGameSettings.add(instanceId);
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
@@ -471,59 +474,59 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GameSettings.Instance getInstanceGameSettings(String id) {
|
||||
if (!loadedInstanceGameSettings.contains(id)) {
|
||||
loadInstanceGameSettings(id);
|
||||
public GameSettings.Instance getInstanceGameSettings(GameInstanceID instanceId) {
|
||||
if (!loadedInstanceGameSettings.contains(instanceId)) {
|
||||
loadInstanceGameSettings(instanceId);
|
||||
}
|
||||
return instanceGameSettings.get(id);
|
||||
return instanceGameSettings.get(instanceId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GameSettings.Instance getInstanceGameSettingsOrCreate(String id) {
|
||||
GameSettings.Instance setting = getInstanceGameSettings(id);
|
||||
public GameSettings.Instance getInstanceGameSettingsOrCreate(GameInstanceID instanceId) {
|
||||
GameSettings.Instance setting = getInstanceGameSettings(instanceId);
|
||||
if (setting == null) {
|
||||
setting = createInstanceGameSettings(id);
|
||||
setting = createInstanceGameSettings(instanceId);
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// Returns whether the instance-specific game settings file cannot be overwritten safely.
|
||||
///
|
||||
/// @param id the instance ID
|
||||
/// @param instanceId the instance ID
|
||||
/// @return whether the instance settings are loaded in read-only mode
|
||||
public boolean isInstanceGameSettingsReadOnly(String id) {
|
||||
if (!loadedInstanceGameSettings.contains(id)) {
|
||||
loadInstanceGameSettings(id);
|
||||
public boolean isInstanceGameSettingsReadOnly(GameInstanceID instanceId) {
|
||||
if (!loadedInstanceGameSettings.contains(instanceId)) {
|
||||
loadInstanceGameSettings(instanceId);
|
||||
}
|
||||
|
||||
return readOnlyInstanceGameSettings.contains(id);
|
||||
return readOnlyInstanceGameSettings.contains(instanceId);
|
||||
}
|
||||
|
||||
/// Backs up and overwrites the instance-specific game settings file with the currently loaded settings.
|
||||
///
|
||||
/// @param id the instance ID
|
||||
public void forceOverwriteInstanceGameSettings(String id) {
|
||||
if (!loadedInstanceGameSettings.contains(id)) {
|
||||
loadInstanceGameSettings(id);
|
||||
/// @param instanceId the instance ID
|
||||
public void forceOverwriteInstanceGameSettings(GameInstanceID instanceId) {
|
||||
if (!loadedInstanceGameSettings.contains(instanceId)) {
|
||||
loadInstanceGameSettings(instanceId);
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = instanceGameSettings.get(id);
|
||||
GameSettings.Instance setting = instanceGameSettings.get(instanceId);
|
||||
if (setting == null) {
|
||||
setting = new GameSettings.Instance();
|
||||
instanceGameSettings.put(id, setting);
|
||||
loadedInstanceGameSettings.add(id);
|
||||
instanceGameSettings.put(instanceId, setting);
|
||||
loadedInstanceGameSettings.add(instanceId);
|
||||
}
|
||||
|
||||
boolean installAutoSave = !setting.isSavable();
|
||||
Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize();
|
||||
Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize();
|
||||
SettingFileUtils.backupInvalidConfig(file);
|
||||
setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA);
|
||||
setting.setSavable(true);
|
||||
setting.setBackupOnNextSave(false);
|
||||
readOnlyInstanceGameSettings.remove(id);
|
||||
saveGameSettings(id);
|
||||
readOnlyInstanceGameSettings.remove(instanceId);
|
||||
saveGameSettings(instanceId);
|
||||
if (installAutoSave) {
|
||||
setting.addListener(a -> saveGameSettings(id));
|
||||
setting.addListener(a -> saveGameSettings(instanceId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +537,33 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
|
||||
}
|
||||
|
||||
public GameSettings.Effective getEffectiveGameSettings(GameInstanceID instanceId) {
|
||||
GameSettings.Instance instance = getInstanceGameSettings(instanceId);
|
||||
return GameSettings.resolve(getParentGameSettings(instance), instance);
|
||||
}
|
||||
|
||||
public void applyDefaultIsolationSetting(GameInstanceID instanceId) {
|
||||
if (!hasInstance(instanceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance instanceSetting = getInstanceGameSettings(instanceId);
|
||||
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
|
||||
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
|
||||
boolean isolated = switch (type) {
|
||||
case NEVER -> false;
|
||||
case ALWAYS -> true;
|
||||
case MODDED -> LibraryAnalyzer.isModded(getResolvedInstanceManifest(instanceId));
|
||||
};
|
||||
|
||||
if (isolated) {
|
||||
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(instanceId);
|
||||
if (setting != null && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
|
||||
saveGameSettings(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
|
||||
public boolean shouldIsolateNewInstance(boolean modded) {
|
||||
GameSettings.Preset preset = getParentGameSettings(null);
|
||||
@@ -545,28 +575,23 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
};
|
||||
}
|
||||
|
||||
/// Applies default isolation to a new instance before the version metadata is saved.
|
||||
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
|
||||
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
|
||||
/// Applies default isolation to a new instance before its manifest is saved.
|
||||
public void applyDefaultIsolationSettingForNewInstance(GameInstanceID instanceId, boolean modded) {
|
||||
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(instanceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = getInstanceGameSettings(id);
|
||||
GameSettings.Instance setting = getInstanceGameSettings(instanceId);
|
||||
if (setting == null) {
|
||||
setting = initInstanceGameSettings(id, new GameSettings.Instance());
|
||||
setting = initInstanceGameSettings(instanceId, new GameSettings.Instance());
|
||||
}
|
||||
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
|
||||
saveGameSettings(id);
|
||||
saveGameSettings(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
public GameSettings.Effective getEffectiveGameSettings(String id) {
|
||||
GameSettings.Instance instance = getInstanceGameSettings(id);
|
||||
return GameSettings.resolve(getParentGameSettings(instance), instance);
|
||||
}
|
||||
|
||||
public Optional<Path> getVersionIconFile(String id) {
|
||||
Path root = getVersionRoot(id);
|
||||
public Optional<Path> getInstanceIconFile(GameInstanceID instanceId) {
|
||||
Path root = getInstanceRoot(instanceId);
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
Path file = root.resolve("icon." + extension);
|
||||
@@ -578,19 +603,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setVersionIconFile(String id, Path iconFile) throws IOException {
|
||||
public void setInstanceIconFile(GameInstanceID instanceId, Path iconFile) throws IOException {
|
||||
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + ext);
|
||||
}
|
||||
|
||||
deleteIconFile(id);
|
||||
deleteIconFile(instanceId);
|
||||
|
||||
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
|
||||
FileUtils.copyFile(iconFile, getInstanceRoot(instanceId).resolve("icon." + ext));
|
||||
}
|
||||
|
||||
public void deleteIconFile(String id) {
|
||||
Path root = getVersionRoot(id);
|
||||
public void deleteIconFile(GameInstanceID instanceId) {
|
||||
Path root = getInstanceRoot(instanceId);
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
Path file = root.resolve("icon." + extension);
|
||||
try {
|
||||
@@ -601,69 +626,69 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public Image getVersionIconImage(@Nullable String id) {
|
||||
if (id == null || !isLoaded())
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
public Image getInstanceIconImage(@Nullable GameInstanceID instanceId) {
|
||||
if (instanceId == null || !isLoaded())
|
||||
return GameInstanceIconType.DEFAULT.getIcon();
|
||||
|
||||
GameSettings.Instance setting = getInstanceGameSettings(id);
|
||||
VersionIconType iconType = setting != null ? Lang.requireNonNullElse(setting.iconProperty().getValue(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT;
|
||||
GameSettings.Instance setting = getInstanceGameSettings(instanceId);
|
||||
GameInstanceIconType iconType = setting != null ? Lang.requireNonNullElse(setting.iconProperty().getValue(), GameInstanceIconType.DEFAULT) : GameInstanceIconType.DEFAULT;
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
Optional<Path> iconFile = getVersionIconFile(id);
|
||||
if (iconType == GameInstanceIconType.DEFAULT) {
|
||||
GameInstanceManifest.Resolved resolvedInstanceManifest = getResolvedInstanceManifest(instanceId);
|
||||
Optional<Path> iconFile = getInstanceIconFile(instanceId);
|
||||
if (iconFile.isPresent()) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
LOG.warning("Failed to load instance icon of " + instanceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (LibraryAnalyzer.isModded(this, version)) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
|
||||
if (LibraryAnalyzer.isModded(resolvedInstanceManifest)) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(resolvedInstanceManifest, null);
|
||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
return GameInstanceIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
return GameInstanceIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC))
|
||||
return VersionIconType.LEGACY_FABRIC.getIcon();
|
||||
return GameInstanceIconType.LEGACY_FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
return GameInstanceIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
return GameInstanceIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
|
||||
return VersionIconType.CLEANROOM.getIcon();
|
||||
return GameInstanceIconType.CLEANROOM.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||
return VersionIconType.CHICKEN.getIcon();
|
||||
return GameInstanceIconType.CHICKEN.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
return GameInstanceIconType.OPTIFINE.getIcon();
|
||||
}
|
||||
|
||||
String gameVersion = getGameVersion(version).orElse(null);
|
||||
String gameVersion = getGameVersion(resolvedInstanceManifest.launchManifest()).orElse(null);
|
||||
if (gameVersion != null) {
|
||||
GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion);
|
||||
if (versionNumber.isAprilFools()) {
|
||||
return VersionIconType.APRIL_FOOLS.getIcon();
|
||||
return GameInstanceIconType.APRIL_FOOLS.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) {
|
||||
return VersionIconType.COMMAND.getIcon();
|
||||
return GameInstanceIconType.COMMAND.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.Old) {
|
||||
return VersionIconType.CRAFT_TABLE.getIcon();
|
||||
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
||||
}
|
||||
}
|
||||
return VersionIconType.GRASS.getIcon();
|
||||
return GameInstanceIconType.GRASS.getIcon();
|
||||
} else {
|
||||
return iconType.getIcon();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveGameSettings(String id) {
|
||||
if (!instanceGameSettings.containsKey(id) || readOnlyInstanceGameSettings.contains(id))
|
||||
public void saveGameSettings(GameInstanceID instanceId) {
|
||||
if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId))
|
||||
return;
|
||||
GameSettings.Instance setting = instanceGameSettings.get(id);
|
||||
GameSettings.Instance setting = instanceGameSettings.get(instanceId);
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize();
|
||||
Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize();
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException e) {
|
||||
@@ -679,19 +704,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
/// Saves instance-specific game settings synchronously.
|
||||
///
|
||||
/// @param id the instance ID
|
||||
/// @param instanceId the instance ID
|
||||
/// @throws IOException if saving the file fails
|
||||
private void saveGameSettingsSync(String id) throws IOException {
|
||||
if (!instanceGameSettings.containsKey(id) || readOnlyInstanceGameSettings.contains(id)) {
|
||||
private void saveGameSettingsSync(GameInstanceID instanceId) throws IOException {
|
||||
if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = instanceGameSettings.get(id);
|
||||
GameSettings.Instance setting = instanceGameSettings.get(instanceId);
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize();
|
||||
Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize();
|
||||
Files.createDirectories(file.getParent());
|
||||
if (setting.isBackupOnNextSave()) {
|
||||
setting.setBackupOnNextSave(false);
|
||||
@@ -709,26 +734,30 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
boolean allowSave) {
|
||||
}
|
||||
|
||||
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
GameSettings.Effective vs = getEffectiveGameSettings(version);
|
||||
public LaunchOptions.Builder getLaunchOptions(GameInstanceID instanceId, JavaRuntime java, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
GameSettings.Effective vs = getEffectiveGameSettings(instanceId);
|
||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
||||
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
||||
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
|
||||
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(instanceId));
|
||||
|
||||
@Nullable Integer maxMemory;
|
||||
if (autoMemory) {
|
||||
maxMemory = noJVMOptions
|
||||
? null
|
||||
: Math.toIntExact(getAutoAllocatedMemory(SystemInfo.getPhysicalMemoryStatus().available()) / 1024L / 1024L);
|
||||
: Math.toIntExact(getAutoAllocatedMemory(
|
||||
SystemInfo.getPhysicalMemoryStatus().available(),
|
||||
java.getPlatform()
|
||||
) / 1024L / 1024L);
|
||||
} else {
|
||||
maxMemory = vs.getMaxMemory();
|
||||
}
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setInstanceId(instanceId)
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setJava(java)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(version)
|
||||
.setVersionName(instanceId.id())
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
|
||||
@@ -773,7 +802,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
builder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
Path json = getModpackConfiguration(version);
|
||||
Path json = getModpackConfiguration(instanceId);
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
String jsonText = Files.readString(json);
|
||||
@@ -792,27 +821,27 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getModpackConfiguration(String version) {
|
||||
return getVersionRoot(version).resolve("modpack.cfg");
|
||||
public Path getModpackConfiguration(GameInstanceID instanceId) {
|
||||
return getInstanceRoot(instanceId).resolve("modpack.cfg");
|
||||
}
|
||||
|
||||
public void markVersionAsModpack(String id) {
|
||||
beingModpackVersions.add(id);
|
||||
public void markInstanceAsModpack(GameInstanceID instanceId) {
|
||||
beingModpackInstances.add(instanceId);
|
||||
}
|
||||
|
||||
public void undoMark(String id) {
|
||||
beingModpackVersions.remove(id);
|
||||
public void undoMark(GameInstanceID instanceId) {
|
||||
beingModpackInstances.remove(instanceId);
|
||||
}
|
||||
|
||||
public void markVersionLaunchedAbnormally(String id) {
|
||||
public void markInstanceLaunchedAbnormally(GameInstanceID instanceId) {
|
||||
try {
|
||||
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
|
||||
Files.createFile(getInstanceRoot(instanceId).resolve(".abnormal"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unmarkVersionLaunchedAbnormally(String id) {
|
||||
Path file = getVersionRoot(id).resolve(".abnormal");
|
||||
public boolean unmarkInstanceLaunchedAbnormally(GameInstanceID instanceId) {
|
||||
Path file = getInstanceRoot(instanceId).resolve(".abnormal");
|
||||
if (Files.isRegularFile(file)) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
@@ -829,39 +858,46 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
|
||||
|
||||
|
||||
// These version ids are forbidden because they may conflict with modpack configuration filenames
|
||||
private static final Set<String> FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList(
|
||||
"modpack", "minecraftinstance", "manifest"));
|
||||
// These instance ids are forbidden because they may conflict with modpack configuration filenames
|
||||
private static final Set<String> FORBIDDEN_INSTANCE_IDS = Set.of("modpack", "minecraftinstance", "manifest");
|
||||
|
||||
public static boolean isValidVersionId(String id) {
|
||||
if (FORBIDDEN_VERSION_IDS.contains(id))
|
||||
public static boolean isValidInstanceId(String id) {
|
||||
if (FORBIDDEN_INSTANCE_IDS.contains(id))
|
||||
return false;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
FORBIDDEN_INSTANCE_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return FileUtils.isNameValidForJar(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given version id conflicts with an existing version.
|
||||
* Returns true if the given instance id conflicts with an existing instance.
|
||||
*/
|
||||
public boolean versionIdConflicts(String id) {
|
||||
public boolean instanceIdConflicts(String instanceId) {
|
||||
try {
|
||||
return instanceIdConflicts(new GameInstanceID(instanceId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean instanceIdConflicts(GameInstanceID id) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// on Windows, filenames are case-insensitive
|
||||
for (String existingId : versions.keySet()) {
|
||||
if (existingId.equalsIgnoreCase(id)) {
|
||||
for (GameInstanceManifest manifest : getInstanceManifests()) {
|
||||
if (manifest.id().toString().equalsIgnoreCase(id.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return versions.containsKey(id);
|
||||
return hasInstance(id);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getAutoAllocatedMemory(long available) {
|
||||
public static long getAutoAllocatedMemory(long available, Platform platform) {
|
||||
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
||||
if (usable <= 0) {
|
||||
return available;
|
||||
@@ -875,7 +911,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
suggested = Math.min(
|
||||
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
|
||||
16L * 1024 * 1024 * 1024);
|
||||
return suggested;
|
||||
return platform.getBits() == Bits.BIT_32
|
||||
? Math.min(suggested, 768 * 1024 * 1024) // https://github.com/HMCL-dev/HMCL/issues/6638
|
||||
: suggested;
|
||||
}
|
||||
|
||||
public static ProxyOption getProxyOption() {
|
||||
|
||||
@@ -37,29 +37,29 @@ import java.util.List;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final Path zipFile;
|
||||
private final String name;
|
||||
private final GameInstanceID instanceId;
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, String name) {
|
||||
public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.dependency = repository.getDependency();
|
||||
this.zipFile = zipFile;
|
||||
this.name = name;
|
||||
this.instanceId = instanceId;
|
||||
this.modpack = modpack;
|
||||
|
||||
Path run = repository.getRunDirectory(name);
|
||||
Path json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Version " + name + " already exists");
|
||||
Path run = repository.getRunDirectory(this.instanceId);
|
||||
Path json = repository.getModpackConfiguration(this.instanceId);
|
||||
if (repository.hasInstance(this.instanceId) && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Instance " + instanceId + " already exists");
|
||||
|
||||
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||
dependents.add(dependency.newGameBuilder().name(this.instanceId).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) repository.removeVersionFromDisk(name);
|
||||
if (event.isFailed()) repository.removeInstanceFromDisk(this.instanceId);
|
||||
});
|
||||
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
@@ -68,12 +68,12 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||
throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance.");
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(this.instanceId)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,9 +89,9 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
|
||||
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
|
||||
GameInstanceManifest originalManifest = JsonUtils.GSON.fromJson(json, GameInstanceManifest.class).withId(instanceId).withJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalManifest, null);
|
||||
Task<GameInstanceManifest> libraryTask = Task.supplyAsync(() -> originalManifest);
|
||||
// reinstall libraries
|
||||
// libraries of Forge and OptiFine should be obtained by installation.
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
|
||||
@@ -42,12 +42,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||
}
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(repository, zipFile, modpack, name));
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new HMCLModpackInstallTask(repository, zipFile, modpack, instanceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -63,21 +63,21 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
GameInstanceManifest game = JsonUtils.fromNonNullJson(gameJson, GameInstanceManifest.class);
|
||||
if (game.jar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.jar().id());
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
|
||||
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, name);
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
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;
|
||||
@@ -84,7 +86,7 @@ public final class LauncherHelper {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private Account account;
|
||||
private final String selectedVersion;
|
||||
private final GameInstanceID selectedInstanceId;
|
||||
private Path scriptFile;
|
||||
private final GameSettings.Effective setting;
|
||||
private LauncherVisibility launcherVisibility;
|
||||
@@ -92,14 +94,14 @@ public final class LauncherHelper {
|
||||
private QuickPlayOption quickPlayOption;
|
||||
private boolean disableOfflineSkin = false;
|
||||
|
||||
public LauncherHelper(HMCLGameRepository repository, Account account, String selectedVersion) {
|
||||
public LauncherHelper(HMCLGameRepository repository, Account account, GameInstanceID selectedInstanceId) {
|
||||
this.repository = Objects.requireNonNull(repository);
|
||||
this.account = Objects.requireNonNull(account);
|
||||
this.selectedVersion = Objects.requireNonNull(selectedVersion);
|
||||
this.setting = repository.getEffectiveGameSettings(selectedVersion);
|
||||
this.selectedInstanceId = selectedInstanceId;
|
||||
this.setting = repository.getEffectiveGameSettings(selectedInstanceId);
|
||||
this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty);
|
||||
this.showLogs = setting.getInheritable(GameSettings::showLogsProperty);
|
||||
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||
this.launchingStepsPane.setTitle(i18n("instance.launch"));
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
|
||||
@@ -132,7 +134,7 @@ public final class LauncherHelper {
|
||||
public void launch() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
LOG.info("Launching game version: " + selectedVersion);
|
||||
LOG.info("Launching game version: " + selectedInstanceId);
|
||||
|
||||
Controllers.dialog(launchingStepsPane);
|
||||
launch0();
|
||||
@@ -148,9 +150,9 @@ public final class LauncherHelper {
|
||||
PROCESSES.removeIf(it -> it.get() == null);
|
||||
|
||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
||||
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
|
||||
AtomicReference<GameInstanceManifest> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedInstanceManifest(selectedInstanceId).launchManifest()));
|
||||
Optional<String> gameVersion = repository.getGameVersion(version.get());
|
||||
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
|
||||
boolean integrityCheck = repository.unmarkInstanceLaunchedAbnormally(selectedInstanceId);
|
||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||
List<String> javaAgents = new ArrayList<>(0);
|
||||
List<String> javaArguments = new ArrayList<>(0);
|
||||
@@ -167,10 +169,10 @@ public final class LauncherHelper {
|
||||
dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck),
|
||||
Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedInstanceId));
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
|
||||
if (provider == null) return null;
|
||||
else return provider.createCompletionTask(dependencyManager, selectedVersion);
|
||||
else return provider.createCompletionTask(dependencyManager, selectedInstanceId);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
@@ -202,7 +204,12 @@ public final class LauncherHelper {
|
||||
})
|
||||
);
|
||||
}).withStage("launch.state.dependencies")
|
||||
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
|
||||
.thenComposeAsync(() -> {
|
||||
if (gameVersion.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new GameVerificationFixTask(dependencyManager, gameVersion.get(), version.get());
|
||||
})
|
||||
.thenComposeAsync(() -> {
|
||||
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|
||||
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|
||||
@@ -228,7 +235,7 @@ public final class LauncherHelper {
|
||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
|
||||
selectedVersion, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
|
||||
selectedInstanceId, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
|
||||
if (disableOfflineSkin) {
|
||||
launchOptionsBuilder.setDaemon(false);
|
||||
}
|
||||
@@ -236,9 +243,39 @@ public final class LauncherHelper {
|
||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
||||
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(javaVersionRef.get().getBinary());
|
||||
try {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg != null) {
|
||||
Object current = reg.queryValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg
|
||||
);
|
||||
if (current == null) {
|
||||
boolean result = reg.setValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg,
|
||||
"GpuPreference=2;"
|
||||
);
|
||||
if (result)
|
||||
LOG.info("Successfully applied high performance GPU preference for java: " + javaPathGpuReg);
|
||||
else
|
||||
LOG.warning("Failed to apply high performance GPU preference for java: " + javaPathGpuReg);
|
||||
} else {
|
||||
LOG.info("GPU preference for %s already exists: %s".formatted(javaPathGpuReg, current));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
||||
}
|
||||
}
|
||||
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedInstanceId), 10));
|
||||
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
@@ -271,7 +308,7 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
runLater(() -> {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
||||
Controllers.dialog(i18n("instance.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
||||
});
|
||||
}
|
||||
}).withFakeProgress(
|
||||
@@ -310,7 +347,7 @@ public final class LauncherHelper {
|
||||
} else if (ex instanceof NotDecompressingNativesException) {
|
||||
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
|
||||
} else if (ex instanceof LibraryDownloadException) {
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().name()) + "\n";
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
@@ -373,7 +410,7 @@ public final class LauncherHelper {
|
||||
message = StringUtils.getStackTrace(ex);
|
||||
}
|
||||
Controllers.dialog(message,
|
||||
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
|
||||
scriptFile == null ? i18n("launch.failed") : i18n("instance.launch_script.failed"),
|
||||
MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
@@ -386,13 +423,13 @@ public final class LauncherHelper {
|
||||
executor.start();
|
||||
}
|
||||
|
||||
private static Task<JavaRuntime> checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, Version version) {
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
private static Task<JavaRuntime> checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null));
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
|
||||
|
||||
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
|
||||
try {
|
||||
return setting.getJava(gameVersion, version);
|
||||
return setting.getJava(gameVersion, manifest);
|
||||
} catch (InterruptedException e) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
@@ -450,7 +487,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (targetJavaVersion == null)
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
targetJavaVersion = manifest.javaVersion();
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||
@@ -480,8 +517,8 @@ public final class LauncherHelper {
|
||||
|
||||
if (java != null) {
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.appliesToVersion(gameVersion, manifest, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, manifest, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violatedMandatoryConstraints.add(constraint);
|
||||
} else {
|
||||
@@ -497,7 +534,7 @@ public final class LauncherHelper {
|
||||
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
|
||||
|
||||
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
|
||||
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
|
||||
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, manifest);
|
||||
if (suggestedJava != null) {
|
||||
FXUtils.runInFX(() -> {
|
||||
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
|
||||
@@ -524,7 +561,7 @@ public final class LauncherHelper {
|
||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
||||
: GameJavaVersion.JAVA_21;
|
||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
gameJavaVersion = manifest.javaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
else
|
||||
@@ -650,14 +687,14 @@ public final class LauncherHelper {
|
||||
|
||||
// Forge 2760~2773 will crash game with LiteLoader.
|
||||
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
boolean hasLiteLoader = manifest.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
|
||||
}
|
||||
|
||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
boolean hasOptiFine = manifest.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
|
||||
}
|
||||
@@ -760,7 +797,10 @@ public final class LauncherHelper {
|
||||
case HIDE_AND_REOPEN:
|
||||
runLater(() -> {
|
||||
Optional.ofNullable(Controllers.getStage())
|
||||
.ifPresent(Stage::show);
|
||||
.ifPresent(stage -> {
|
||||
stage.setIconified(false);
|
||||
stage.show();
|
||||
});
|
||||
});
|
||||
break;
|
||||
case KEEP:
|
||||
@@ -798,7 +838,7 @@ public final class LauncherHelper {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
private final GameInstanceManifest manifest;
|
||||
private final LaunchOptions launchOptions;
|
||||
private ManagedProcess process;
|
||||
private volatile boolean lwjgl;
|
||||
@@ -810,9 +850,9 @@ public final class LauncherHelper {
|
||||
private Thread submitLogThread;
|
||||
private LinkedBlockingQueue<Log> logBuffer;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
public HMCLProcessListener(HMCLGameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.manifest = manifest;
|
||||
this.launchOptions = launchOptions;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
@@ -895,8 +935,10 @@ public final class LauncherHelper {
|
||||
runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().hide();
|
||||
@Nullable Stage stage = Controllers.getStage();
|
||||
if (stage != null) {
|
||||
stage.setIconified(false);
|
||||
stage.hide();
|
||||
launchingLatch.countDown();
|
||||
}
|
||||
});
|
||||
@@ -995,8 +1037,8 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
repository.markVersionLaunchedAbnormally(version.getId());
|
||||
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||
repository.markInstanceLaunchedAbnormally(manifest.id());
|
||||
runLater(() -> new GameCrashWindow(process, exitType, repository, manifest, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
|
||||
@@ -20,9 +20,10 @@ package org.jackhuang.hmcl.game;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.ui.versions.ModTranslations;
|
||||
import org.jackhuang.hmcl.ui.instances.ModTranslations;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -40,6 +41,16 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public String getApiBaseUrl() {
|
||||
return getBackedRemoteModRepository().getApiBaseUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return getBackedRemoteModRepository().getBaseUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
@@ -49,7 +60,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
|
||||
|
||||
int count = 0;
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(getType()).searchMod(searchFilter)) {
|
||||
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName()));
|
||||
if (StringUtils.isNotBlank(englishSearchFilter)) {
|
||||
englishSearchFiltersSet.add(englishSearchFilter);
|
||||
@@ -82,7 +93,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
continue;
|
||||
}
|
||||
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteAddon.slug());
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteAddon.slug());
|
||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
||||
searchResultArray[chineseIndex++] = remoteAddon;
|
||||
} else {
|
||||
@@ -94,7 +105,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.slug());
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteMod.slug());
|
||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||
}
|
||||
@@ -123,17 +134,27 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(downloadProvider, id);
|
||||
public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getAddonById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.File getModFile(String modId, String fileId) throws IOException {
|
||||
return getBackedRemoteModRepository().getModFile(modId, fileId);
|
||||
public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException {
|
||||
return getBackedRemoteModRepository().getAddonFile(projectId, fileId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteAddon.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException {
|
||||
return getBackedRemoteModRepository().getAddonChangelog(downloadProvider, addonId, versionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersionPageUrl(RemoteAddon.Version version) throws IOException {
|
||||
return getBackedRemoteModRepository().getVersionPageUrl(version);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
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;
|
||||
@@ -42,22 +41,22 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(
|
||||
Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript,
|
||||
Path zipFile, DefaultGameRepository repository, GameInstanceID instanceId, String logs, String launchScript,
|
||||
PathMatcher logMatcher) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||
List<String> versions = new ArrayList<>();
|
||||
Path runDirectory = repository.getRunDirectory(instanceId);
|
||||
Path baseDirectory = repository.getBaseDirectory();
|
||||
List<GameInstanceID> instances = new ArrayList<>();
|
||||
|
||||
String currentVersionId = versionId;
|
||||
HashSet<String> resolvedSoFar = new HashSet<>();
|
||||
GameInstanceID currentInstanceId = instanceId;
|
||||
HashSet<GameInstanceID> resolvedSoFar = new HashSet<>();
|
||||
while (true) {
|
||||
if (resolvedSoFar.contains(currentVersionId)) break;
|
||||
resolvedSoFar.add(currentVersionId);
|
||||
Version currentVersion = gameRepository.getVersion(currentVersionId);
|
||||
versions.add(currentVersionId);
|
||||
if (resolvedSoFar.contains(currentInstanceId)) break;
|
||||
resolvedSoFar.add(currentInstanceId);
|
||||
GameInstanceManifest currentVersion = repository.getInstanceManifest(currentInstanceId);
|
||||
instances.add(currentInstanceId);
|
||||
|
||||
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
|
||||
currentVersionId = currentVersion.getInheritsFrom();
|
||||
if (currentVersion.inheritsFrom() != null) {
|
||||
currentInstanceId = currentVersion.inheritsFrom();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -74,10 +73,10 @@ public final class LogExporter {
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||
|
||||
for (String id : versions) {
|
||||
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
|
||||
if (Files.exists(versionJson)) {
|
||||
zipper.putFile(versionJson, id + ".json");
|
||||
for (GameInstanceID id : instances) {
|
||||
Path instanceJson = repository.getInstanceJson(id);
|
||||
if (Files.exists(instanceJson)) {
|
||||
zipper.putFile(instanceJson, id + ".json");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -156,13 +156,13 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
repository.markVersionAsModpack(name);
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) {
|
||||
repository.markInstanceAsModpack(instanceId);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refreshVersions();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name);
|
||||
repository.undoMark(name);
|
||||
repository.refresh();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
repository.undoMark(instanceId);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
@@ -175,7 +175,7 @@ public final class ModpackHelper {
|
||||
}
|
||||
};
|
||||
|
||||
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name)
|
||||
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
@@ -200,13 +200,13 @@ public final class ModpackHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, String name, Modpack modpack, String iconUrl) {
|
||||
repository.markVersionAsModpack(name);
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, String iconUrl) {
|
||||
repository.markInstanceAsModpack(instanceId);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refreshVersions();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name);
|
||||
repository.undoMark(name);
|
||||
repository.refresh();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
repository.undoMark(instanceId);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
@@ -220,45 +220,45 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
|
||||
.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.javafx(), success, failure)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(repository, name, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name))
|
||||
.thenComposeAsync(repository.refreshVersionsAsync())
|
||||
return new ModpackUpdateTask(repository, instanceId, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(repository.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.thenComposeAsync(repository.refreshVersionsAsync());
|
||||
return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
else
|
||||
return provider.createUpdateTask(repository.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(repository.refreshVersionsAsync());
|
||||
return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
}
|
||||
|
||||
public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
|
||||
@@ -333,25 +333,25 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) {
|
||||
private static Task<Void> createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
||||
ModpackHelper.applyCommandAndJvmSettings(manifest, setting);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) {
|
||||
private static Task<Void> createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
||||
ModpackHelper.toGameSettings(manifest, setting);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, String version) {
|
||||
private static Task<Void> createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, GameInstanceID instanceId) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
GameSettings.Effective effective = repository.getEffectiveGameSettings(version);
|
||||
GameSettings.Effective effective = repository.getEffectiveGameSettings(instanceId);
|
||||
if (manifest.getLaunchInfo().getMinMemory() > effective.getMaxMemory()) {
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
||||
setting.getOverrideProperties().addAll(List.of(
|
||||
GameSettings.PROPERTY_AUTO_MEMORY,
|
||||
GameSettings.PROPERTY_MIN_MEMORY,
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.game.JavaVersionConstraint;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -315,13 +315,13 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
|
||||
return findSuitableJava(getAllJava(), gameVersion, version);
|
||||
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, GameInstanceManifest manifest) throws InterruptedException {
|
||||
return findSuitableJava(getAllJava(), gameVersion, manifest);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
|
||||
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
|
||||
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, GameInstanceManifest manifest) {
|
||||
LibraryAnalyzer analyzer = manifest != null ? LibraryAnalyzer.analyze(manifest, gameVersion != null ? gameVersion.toString() : null) : null;
|
||||
|
||||
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
@@ -342,8 +342,8 @@ public final class JavaManager {
|
||||
boolean violationSuggested = false;
|
||||
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.appliesToVersion(gameVersion, manifest, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, manifest, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violationMandatory = true;
|
||||
} else {
|
||||
|
||||
@@ -34,8 +34,6 @@ 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
|
||||
@@ -118,7 +116,7 @@ public final class GameDirectory implements Observable {
|
||||
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
|
||||
this.legacyGameSettings = new SimpleObjectProperty<>(this, "legacyGameSettings", legacyGameSettings);
|
||||
|
||||
addPropertyChangedListener(onInvalidating(this::invalidate));
|
||||
addPropertyChangedListener(arg -> invalidate());
|
||||
}
|
||||
|
||||
/// Returns a debug string containing the game directory path and display metadata.
|
||||
|
||||
@@ -23,7 +23,8 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.util.PortablePath;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -141,10 +142,10 @@ public final class GameDirectoryManager {
|
||||
private static final ObjectProperty<@UnknownNullability HMCLGameRepository> selectedRepository = new SimpleObjectProperty<>(GameDirectoryManager.class, "selectedRepository");
|
||||
|
||||
/// The selected instance ID projected from the selected repository.
|
||||
private static final ReadOnlyStringWrapper selectedInstance = new ReadOnlyStringWrapper(GameDirectoryManager.class, "selectedInstance");
|
||||
private static final ReadOnlyObjectWrapper<GameInstanceID> selectedInstance = new ReadOnlyObjectWrapper<>(GameDirectoryManager.class, "selectedInstance");
|
||||
|
||||
/// Updates [#selectedInstance] when the selected repository changes its selected instance.
|
||||
private static final ChangeListener<String> selectedRepositoryInstanceListener =
|
||||
private static final ChangeListener<GameInstanceID> selectedRepositoryInstanceListener =
|
||||
(observable, oldValue, newValue) -> selectedInstance.set(newValue);
|
||||
|
||||
/// Initializes game directory state from the stores loaded by [SettingsManager].
|
||||
@@ -206,11 +207,11 @@ public final class GameDirectoryManager {
|
||||
selectedRepository.set(repository);
|
||||
selectedInstance.set(repository.getSelectedInstance());
|
||||
repository.selectedInstanceProperty().addListener(selectedRepositoryInstanceListener);
|
||||
repository.refreshVersionsAsync().start();
|
||||
repository.refreshAsync().start();
|
||||
});
|
||||
selectedGameDirectory.set(currentGameDirectory != null ? currentGameDirectory : mergedGameDirectories.get(0));
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
|
||||
EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> {
|
||||
runInFX(() -> {
|
||||
@Nullable HMCLGameRepository repository = selectedRepository.get();
|
||||
if (repository != null && repository == event.getSource()) {
|
||||
@@ -479,17 +480,17 @@ public final class GameDirectoryManager {
|
||||
}
|
||||
|
||||
/// Returns the selected instance property projected from the selected repository.
|
||||
public static ReadOnlyStringProperty selectedInstanceProperty() {
|
||||
public static ReadOnlyObjectProperty<@Nullable GameInstanceID> selectedInstanceProperty() {
|
||||
return selectedInstance.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Returns the selected instance ID for the selected repository.
|
||||
public static @Nullable String getSelectedInstance() {
|
||||
public static @Nullable GameInstanceID getSelectedInstance() {
|
||||
return getSelectedRepository().getSelectedInstance();
|
||||
}
|
||||
|
||||
/// Sets the selected instance ID for the selected repository.
|
||||
public static void setSelectedInstance(@Nullable String instance) {
|
||||
public static void setSelectedInstance(@Nullable GameInstanceID instance) {
|
||||
getSelectedRepository().setSelectedInstance(instance);
|
||||
}
|
||||
|
||||
|
||||
+10
-11
@@ -21,7 +21,7 @@ import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
public enum GameInstanceIconType {
|
||||
DEFAULT("/assets/img/grass.png"),
|
||||
|
||||
GRASS("/assets/img/grass.png"),
|
||||
@@ -42,22 +42,21 @@ public enum VersionIconType {
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
public static GameInstanceIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGE -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case LEGACY_FABRIC -> VersionIconType.LEGACY_FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
case CLEANROOM -> VersionIconType.CLEANROOM;
|
||||
default -> VersionIconType.COMMAND;
|
||||
case FORGE -> GameInstanceIconType.FORGE;
|
||||
case NEO_FORGE -> GameInstanceIconType.NEO_FORGE;
|
||||
case FABRIC -> GameInstanceIconType.FABRIC;
|
||||
case QUILT -> GameInstanceIconType.QUILT;
|
||||
case LITE_LOADER -> GameInstanceIconType.CHICKEN;
|
||||
case CLEANROOM -> GameInstanceIconType.CLEANROOM;
|
||||
default -> GameInstanceIconType.COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
VersionIconType(String resourceUrl) {
|
||||
GameInstanceIconType(String resourceUrl) {
|
||||
this.resourceUrl = resourceUrl;
|
||||
}
|
||||
|
||||
@@ -152,10 +152,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
|
||||
/// The icon of the instance.
|
||||
@SerializedName("icon")
|
||||
private final SettingProperty<VersionIconType> icon = newSettingProperty("icon", VersionIconType.DEFAULT);
|
||||
private final SettingProperty<GameInstanceIconType> icon = newSettingProperty("icon", GameInstanceIconType.DEFAULT);
|
||||
|
||||
/// Returns the instance icon property.
|
||||
public SettingProperty<VersionIconType> iconProperty() {
|
||||
public SettingProperty<GameInstanceIconType> iconProperty() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -643,6 +643,19 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
return vulkanRenderer;
|
||||
}
|
||||
|
||||
/// Property name for high performance GPU preference.
|
||||
public static final String PROPERTY_HIGH_PERFORMANCE = "highPerformance";
|
||||
|
||||
/// Whether to launch games with high-performance GPU.
|
||||
@SerializedName(PROPERTY_HIGH_PERFORMANCE)
|
||||
private final InheritableProperty<Boolean> highPerformance =
|
||||
newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, false);
|
||||
|
||||
/// Returns the high performance GPU preference property.
|
||||
public InheritableProperty<Boolean> highPerformanceProperty() {
|
||||
return highPerformance;
|
||||
}
|
||||
|
||||
/// Property name for customized environment variables.
|
||||
public static final String PROPERTY_ENVIRONMENT_VARIABLES = "environmentVariables";
|
||||
|
||||
@@ -916,11 +929,11 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
}
|
||||
|
||||
/// Finds the effective Java runtime.
|
||||
public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable Version version) throws InterruptedException {
|
||||
public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable GameInstanceManifest manifest) throws InterruptedException {
|
||||
JavaVersionType javaVersionType = getInheritable(GameSettings::javaTypeProperty);
|
||||
switch (javaVersionType) {
|
||||
case AUTO:
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
return JavaManager.findSuitableJava(gameVersion, manifest);
|
||||
case CUSTOM:
|
||||
try {
|
||||
return JavaManager.getJava(Path.of(getInheritable(GameSettings::customJavaPathProperty)));
|
||||
@@ -930,7 +943,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
case VERSION: {
|
||||
String javaVersion = getInheritable(GameSettings::customJavaVersionProperty);
|
||||
if (StringUtils.isBlank(javaVersion)) {
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
return JavaManager.findSuitableJava(gameVersion, manifest);
|
||||
}
|
||||
|
||||
int majorVersion = -1;
|
||||
@@ -948,13 +961,13 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
|
||||
.filter(it -> it.getParsedVersion() == finalMajorVersion)
|
||||
.collect(Collectors.toList());
|
||||
return JavaManager.findSuitableJava(allJava, gameVersion, version);
|
||||
return JavaManager.findSuitableJava(allJava, gameVersion, manifest);
|
||||
}
|
||||
case DETECTED: {
|
||||
DetectedJava detectedJava = getInheritable(GameSettings::detectedJavaProperty);
|
||||
String javaVersion = detectedJava.version();
|
||||
if (StringUtils.isBlank(javaVersion)) {
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
return JavaManager.findSuitableJava(gameVersion, manifest);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(detectedJava.pathHash())) {
|
||||
|
||||
@@ -156,7 +156,14 @@ final class JsonSettingFile<T extends ObservableSetting & JsonSchemaSetting> {
|
||||
///
|
||||
/// @param value the settings object to observe
|
||||
void installAutoSave(T value) {
|
||||
value.addListener(source -> save(value));
|
||||
value.addListener(observable -> {
|
||||
if (value.shouldSaveImmediately(observable)) {
|
||||
save(value);
|
||||
value.setSavePending(false);
|
||||
} else {
|
||||
value.setSavePending(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Saves a settings object.
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AccountID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
|
||||
import org.jackhuang.hmcl.theme.BuiltinBackground;
|
||||
@@ -480,17 +481,19 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
|
||||
/// Whether UI animations are disabled.
|
||||
@SerializedName("animationDisabled")
|
||||
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
|
||||
FXUtils.REDUCED_MOTION == Boolean.TRUE
|
||||
|| !JavaRuntime.CURRENT_JIT_ENABLED
|
||||
|| !FXUtils.GPU_ACCELERATION_ENABLED
|
||||
);
|
||||
private final ObjectProperty<@Nullable Boolean> animationDisabled = new SimpleObjectProperty<>();
|
||||
|
||||
/// Returns the UI animation disable property.
|
||||
public BooleanProperty animationDisabledProperty() {
|
||||
public ObjectProperty<@Nullable Boolean> 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.
|
||||
@@ -621,34 +624,34 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
///
|
||||
/// This field is owned by [GameDirectoryManager]. Code outside [GameDirectoryManager] should not modify it directly.
|
||||
@SerializedName(PROPERTY_SELECTED_INSTANCE)
|
||||
private final ObservableMap<GameDirectoryID, String> selectedInstance = FXCollections.observableHashMap();
|
||||
private final ObservableMap<GameDirectoryID, GameInstanceID> selectedInstance = FXCollections.observableHashMap();
|
||||
|
||||
/// Returns selected instance IDs keyed by game directory ID.
|
||||
///
|
||||
/// The map stores persisted selected instance values by game directory ID.
|
||||
public ObservableMap<GameDirectoryID, String> getSelectedInstance() {
|
||||
public ObservableMap<GameDirectoryID, GameInstanceID> getSelectedInstance() {
|
||||
return selectedInstance;
|
||||
}
|
||||
|
||||
/// Returns the selected instance ID for the given game directory ID.
|
||||
///
|
||||
/// The value is loaded by the game repository for the matching game directory.
|
||||
public @Nullable String getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) {
|
||||
public @Nullable GameInstanceID getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) {
|
||||
return gameDirectoryId != null ? selectedInstance.get(gameDirectoryId) : null;
|
||||
}
|
||||
|
||||
/// Sets the selected instance ID for the given game directory ID.
|
||||
///
|
||||
/// Blank values remove the persisted selected instance entry.
|
||||
public void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable String selectedInstance) {
|
||||
public void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable GameInstanceID selectedInstance) {
|
||||
if (gameDirectoryId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(selectedInstance)) {
|
||||
this.selectedInstance.remove(gameDirectoryId);
|
||||
} else {
|
||||
if (selectedInstance != null) {
|
||||
this.selectedInstance.put(gameDirectoryId, selectedInstance);
|
||||
} else {
|
||||
this.selectedInstance.remove(gameDirectoryId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -54,6 +55,15 @@ 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);
|
||||
|
||||
@@ -73,22 +73,22 @@ public final class LegacyGameSettingsMigrator {
|
||||
}
|
||||
|
||||
/// Legacy `VersionIconType` ordinal order used by old local settings.
|
||||
private static final VersionIconType @Unmodifiable [] LEGACY_VERSION_ICON_TYPES = {
|
||||
VersionIconType.DEFAULT,
|
||||
VersionIconType.GRASS,
|
||||
VersionIconType.CHEST,
|
||||
VersionIconType.CHICKEN,
|
||||
VersionIconType.COMMAND,
|
||||
VersionIconType.OPTIFINE,
|
||||
VersionIconType.CRAFT_TABLE,
|
||||
VersionIconType.FABRIC,
|
||||
VersionIconType.FORGE,
|
||||
VersionIconType.NEO_FORGE,
|
||||
VersionIconType.FURNACE,
|
||||
VersionIconType.QUILT,
|
||||
VersionIconType.APRIL_FOOLS,
|
||||
VersionIconType.CLEANROOM,
|
||||
VersionIconType.LEGACY_FABRIC
|
||||
private static final GameInstanceIconType @Unmodifiable [] LEGACY_VERSION_ICON_TYPES = {
|
||||
GameInstanceIconType.DEFAULT,
|
||||
GameInstanceIconType.GRASS,
|
||||
GameInstanceIconType.CHEST,
|
||||
GameInstanceIconType.CHICKEN,
|
||||
GameInstanceIconType.COMMAND,
|
||||
GameInstanceIconType.OPTIFINE,
|
||||
GameInstanceIconType.CRAFT_TABLE,
|
||||
GameInstanceIconType.FABRIC,
|
||||
GameInstanceIconType.FORGE,
|
||||
GameInstanceIconType.NEO_FORGE,
|
||||
GameInstanceIconType.FURNACE,
|
||||
GameInstanceIconType.QUILT,
|
||||
GameInstanceIconType.APRIL_FOOLS,
|
||||
GameInstanceIconType.CLEANROOM,
|
||||
GameInstanceIconType.LEGACY_FABRIC
|
||||
};
|
||||
|
||||
/// Legacy `JavaVersionType` ordinal order used by old game settings.
|
||||
@@ -130,9 +130,9 @@ public final class LegacyGameSettingsMigrator {
|
||||
/// @return the migrated instance setting, or `null` when no legacy file can be migrated
|
||||
public static @Nullable InstanceMigrationResult migrateInstanceGameSettings(
|
||||
HMCLGameRepository repository,
|
||||
String instanceId,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable GameSettingsPresetID parent) {
|
||||
Path instanceRoot = repository.getVersionRoot(instanceId);
|
||||
Path instanceRoot = repository.getInstanceRoot(instanceId);
|
||||
Path file = instanceRoot.resolve(LEGACY_INSTANCE_SETTINGS_FILENAME);
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
@@ -488,10 +488,10 @@ public final class LegacyGameSettingsMigrator {
|
||||
}
|
||||
|
||||
/// Parses the legacy icon selection with frozen ordinal order.
|
||||
private static VersionIconType parseLegacyVersionIconType(@Nullable JsonObject source) {
|
||||
private static GameInstanceIconType parseLegacyVersionIconType(@Nullable JsonObject source) {
|
||||
JsonPrimitive primitive = JsonUtils.getPrimitive(source, "versionIcon");
|
||||
if (primitive == null) {
|
||||
return VersionIconType.DEFAULT;
|
||||
return GameInstanceIconType.DEFAULT;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -499,11 +499,11 @@ public final class LegacyGameSettingsMigrator {
|
||||
int index = primitive.getAsInt();
|
||||
return index >= 0 && index < LEGACY_VERSION_ICON_TYPES.length
|
||||
? LEGACY_VERSION_ICON_TYPES[index]
|
||||
: VersionIconType.DEFAULT;
|
||||
: GameInstanceIconType.DEFAULT;
|
||||
}
|
||||
|
||||
String value = primitive.getAsString();
|
||||
for (VersionIconType iconType : LEGACY_VERSION_ICON_TYPES) {
|
||||
for (GameInstanceIconType iconType : LEGACY_VERSION_ICON_TYPES) {
|
||||
if (iconType.name().equalsIgnoreCase(value)) {
|
||||
return iconType;
|
||||
}
|
||||
@@ -511,7 +511,7 @@ public final class LegacyGameSettingsMigrator {
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
|
||||
return VersionIconType.DEFAULT;
|
||||
return GameInstanceIconType.DEFAULT;
|
||||
}
|
||||
|
||||
/// Reads an enum property from either ordinal or name.
|
||||
|
||||
@@ -55,6 +55,20 @@ 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,19 +28,12 @@ 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
|
||||
*/
|
||||
@@ -67,23 +60,8 @@ public final class StyleSheets {
|
||||
});
|
||||
}
|
||||
|
||||
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 toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
@@ -133,7 +111,7 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString(), defaultCss);
|
||||
return toStyleSheetUri(builder.toString());
|
||||
}
|
||||
|
||||
private static String getBrightnessStyleSheet() {
|
||||
@@ -184,7 +162,7 @@ public final class StyleSheets {
|
||||
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
|
||||
|
||||
builder.append("}\n");
|
||||
return toStyleSheetUri(builder.toString(), blueCss);
|
||||
return toStyleSheetUri(builder.toString());
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -24,21 +24,12 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -49,19 +40,18 @@ import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.decorator.Decorator;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.terracotta.TerracottaPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.ui.instances.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameInstancePage;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -73,8 +63,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
@@ -90,26 +83,11 @@ public final class Controllers {
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
public static final String APRIL_FOOLS = "aprilFools";
|
||||
|
||||
private static final int CUSTOM_DECORATION_SHADOW_SIZE = 8;
|
||||
private static final int CUSTOM_DECORATION_SHADOW_EXTENT = CUSTOM_DECORATION_SHADOW_SIZE * 2;
|
||||
|
||||
public static final int MIN_CONTENT_WIDTH = 800 + 2; // bg width + border width*2
|
||||
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 Screen SCREEN = Screen.getPrimary();
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
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;
|
||||
private static VersionPage versionPage;
|
||||
private static GameInstancePage gameInstancePage;
|
||||
private static Lazy<GameListPage> gameListPage = new Lazy<>(GameListPage::new);
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
/// The coordinator for the main window's scene graph and navigation stack.
|
||||
private static @Nullable Decorator decorator;
|
||||
private static DownloadPage downloadPage;
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
@@ -133,27 +111,27 @@ public final class Controllers {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
public static Scene getScene() {
|
||||
return scene;
|
||||
}
|
||||
|
||||
public static Stage getStage() {
|
||||
return stage;
|
||||
/// Returns the stage currently attached to the main-window decorator.
|
||||
///
|
||||
/// @return the current stage, or `null` before initialization or after detachment
|
||||
public static @Nullable Stage getStage() {
|
||||
@Nullable Decorator currentDecorator = decorator;
|
||||
return currentDecorator == null ? null : currentDecorator.getStage();
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
if (versionPage == null) {
|
||||
versionPage = new VersionPage();
|
||||
public static GameInstancePage getGameInstancePage() {
|
||||
if (gameInstancePage == null) {
|
||||
gameInstancePage = new GameInstancePage();
|
||||
}
|
||||
return versionPage;
|
||||
return gameInstancePage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareVersionPage() {
|
||||
if (versionPage == null) {
|
||||
LOG.info("Prepare the version page");
|
||||
versionPage = FXUtils.prepareNode(new VersionPage());
|
||||
public static void prepareGameInstancePage() {
|
||||
if (gameInstancePage == null) {
|
||||
LOG.info("Prepare the game instance page");
|
||||
gameInstancePage = FXUtils.prepareNode(new GameInstancePage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,71 +187,24 @@ public final class Controllers {
|
||||
return terracottaPage.get();
|
||||
}
|
||||
|
||||
/// Returns the initialized main-window decorator.
|
||||
///
|
||||
/// @return the application-wide main-window decorator
|
||||
@FXThread
|
||||
public static DecoratorController getDecorator() {
|
||||
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 Decorator getDecorator() {
|
||||
return Objects.requireNonNull(decorator, "Main window is not initialized");
|
||||
}
|
||||
|
||||
/// Releases stage-specific listeners and retained window ownership before application shutdown.
|
||||
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;
|
||||
if (decorator != null) {
|
||||
decorator.detachStage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the main application stage, scene graph, and background services.
|
||||
///
|
||||
/// @param stage the primary application stage, which must not have been shown
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
@@ -285,102 +216,16 @@ 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.getOutputScaleX() > 1) {
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Screen.getPrimary().getOutputScaleX() > 1) {
|
||||
LOG.info("Disable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "false");
|
||||
}
|
||||
}
|
||||
|
||||
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 = toStageWidth(initContentWidth);
|
||||
double initHeight = toStageHeight(initContentHeight);
|
||||
|
||||
{
|
||||
double initContentX = state().getX() * SCREEN.getBounds().getWidth();
|
||||
double initContentY = state().getY() * SCREEN.getBounds().getHeight();
|
||||
|
||||
boolean invalid = true;
|
||||
double border = 20D;
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
Rectangle2D bound = screen.getBounds();
|
||||
|
||||
if (bound.getMinX() + border <= initContentX + initContentWidth
|
||||
&& initContentX <= bound.getMaxX() - border
|
||||
&& bound.getMinY() + border <= initContentY
|
||||
&& initContentY <= bound.getMaxY() - border) {
|
||||
invalid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
initContentX = (0.5D - initContentWidth / SCREEN.getBounds().getWidth() / 2)
|
||||
* SCREEN.getBounds().getWidth();
|
||||
initContentY = (0.5D - initContentHeight / SCREEN.getBounds().getHeight() / 2)
|
||||
* SCREEN.getBounds().getHeight();
|
||||
}
|
||||
|
||||
double initX = toStageX(initContentX);
|
||||
double initY = toStageY(initContentY);
|
||||
stage.setX(initX);
|
||||
stage.setY(initY);
|
||||
stageX.set(initX);
|
||||
stageY.set(initY);
|
||||
}
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
stageHeight.set(initHeight);
|
||||
stageWidth.set(initWidth);
|
||||
stage.xProperty().addListener(weakListener);
|
||||
stage.yProperty().addListener(weakListener);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
stage.widthProperty().addListener(weakListener);
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator = new DecoratorController(stage, getRootPage());
|
||||
decorator = new Decorator(getRootPage());
|
||||
Scene mainScene = decorator.attachStage(stage);
|
||||
getRootPage().getMainPage().showUpdateProperty().bind(UpdateChecker.checkingUpdateProperty().not().and(UpdateChecker.outdatedProperty()));
|
||||
getRootPage().getMainPage().showUpdateDialogProperty().bind(
|
||||
decorator.backableProperty().not()
|
||||
@@ -396,36 +241,10 @@ public final class Controllers {
|
||||
|
||||
Lang.thread(JavaManager::initialize, "Search Java", true);
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinWidth(MIN_WIDTH);
|
||||
stage.setMinHeight(MIN_HEIGHT);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
StyleSheets.init(scene);
|
||||
StyleSheets.init(mainScene);
|
||||
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(600),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
if (!Architecture.SYSTEM_ARCH.isX86() && SettingsManager.userState().platformPromptVersionProperty().get() < 1) {
|
||||
Runnable continueAction = () -> {
|
||||
@@ -585,7 +404,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(
|
||||
@@ -694,6 +513,38 @@ public final class Controllers {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
/// Shows `directoryChooser` with the current main window as its owner.
|
||||
///
|
||||
/// @param directoryChooser the chooser to show
|
||||
/// @return the selected directory, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {
|
||||
return FileUtils.toPath(directoryChooser.showDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for opening one file with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected file, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showOpenDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for saving one file with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected file, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showSaveDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for opening multiple files with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected files, or `null` if the chooser is cancelled
|
||||
public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(getStage()));
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
switch (href) {
|
||||
@@ -703,7 +554,7 @@ public final class Controllers {
|
||||
break;
|
||||
case "hmcl://game/launch":
|
||||
var repository = GameDirectoryManager.getSelectedRepository();
|
||||
Versions.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep);
|
||||
Instances.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -711,22 +562,39 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openUriOrCopy(@Nullable URI uri) {
|
||||
if (uri == null) return;
|
||||
openUriOrCopy(uri.toString());
|
||||
}
|
||||
|
||||
public static void openUriOrCopy(@Nullable String uri) {
|
||||
if (uri == null) return;
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("web.open_in_browser", uri),
|
||||
i18n("message.confirm"),
|
||||
MessageDialogPane.MessageType.QUESTION
|
||||
)
|
||||
.addAction(i18n("button.copy"), () -> FXUtils.copyText(uri))
|
||||
.yesOrNo(() -> FXUtils.openLink(uri), null)
|
||||
.build();
|
||||
dialog(dialog);
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
return decorator == null;
|
||||
}
|
||||
|
||||
/// Releases controller-owned pages, window state, and JavaFX helper resources.
|
||||
public static void shutdown() {
|
||||
onApplicationStop();
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
gameInstancePage = null;
|
||||
gameListPage = null;
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
terracottaPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
|
||||
FXUtils.shutdown();
|
||||
}
|
||||
|
||||
@@ -49,17 +49,31 @@ public final class DialogUtils {
|
||||
|
||||
public static final String PROPERTY_DIALOG_SHOW_LATER = DialogUtils.class.getName() + ".dialog.showLater";
|
||||
|
||||
/// Shows content in the main-window dialog stack owned by `decorator`.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param content the dialog content
|
||||
public static void show(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> show(decorator, content));
|
||||
StackPane dialogContainer = decorator.getDialogContainer();
|
||||
if (decorator.getRoot().getScene() == null) {
|
||||
Platform.runLater(() -> showInDecorator(decorator, dialogContainer, content));
|
||||
return;
|
||||
}
|
||||
|
||||
show(decorator.getDrawerWrapper(), content, (dialog) -> {
|
||||
showInDecorator(decorator, dialogContainer, content);
|
||||
}
|
||||
|
||||
/// Shows content in a decorator's resolved dialog container.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param dialogContainer the container resolved before any deferred execution
|
||||
/// @param content the dialog content
|
||||
private static void showInDecorator(Decorator decorator, StackPane dialogContainer, Node content) {
|
||||
show(dialogContainer, content, dialog -> {
|
||||
JFXDialogPane pane = (JFXDialogPane) dialog.getContent();
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(pane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
dialog.setDialogContainer(dialogContainer);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,12 +139,18 @@ public final class DialogUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues content in the main-window dialog stack owned by `decorator`.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param content the dialog content
|
||||
public static void showLater(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> showLater(decorator, content));
|
||||
StackPane dialogContainer = decorator.getDialogContainer();
|
||||
Runnable showDialogAction = () -> showInDecorator(decorator, dialogContainer, content);
|
||||
if (decorator.getRoot().getScene() == null) {
|
||||
Platform.runLater(() -> showLater(dialogContainer, showDialogAction));
|
||||
return;
|
||||
}
|
||||
showLater(decorator.getDrawerWrapper(), () -> show(decorator, content));
|
||||
showLater(dialogContainer, showDialogAction);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -32,6 +32,7 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
@@ -44,6 +45,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.TableViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -83,6 +85,7 @@ import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
@@ -1713,4 +1716,62 @@ public final class FXUtils {
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri) {
|
||||
var textFlow = new HTMLRenderer(Controllers::openUriOrCopy).appendNode(Jsoup.parse(changelogHtml, baseUri)).mergeLineBreaks().render();
|
||||
textFlow.getStyleClass().add("addon-changelog");
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
/// Be cautious when dealing with large amounts of data
|
||||
public static <S> TableView<S> newAutoHeightTable(ObservableList<S> items) {
|
||||
return new AutoHeightTableView<>(items);
|
||||
}
|
||||
|
||||
private static final class AutoHeightTableView<S> extends TableView<S> {
|
||||
|
||||
public AutoHeightTableView() {
|
||||
super();
|
||||
}
|
||||
|
||||
public AutoHeightTableView(ObservableList<S> items) {
|
||||
super(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new AutoSizeSkin<>(this);
|
||||
}
|
||||
|
||||
private static final class AutoSizeSkin<S> extends TableViewSkin<S> {
|
||||
|
||||
private final VirtualFlow<TableRow<S>> flow;
|
||||
|
||||
public AutoSizeSkin(TableView<S> control) {
|
||||
super(control);
|
||||
this.flow = getVirtualFlow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
double fixedCellSize = getSkinnable().getFixedCellSize();
|
||||
double w = width - rightInset - leftInset;
|
||||
int itemCount = getItemCount();
|
||||
double headerHeight = getTableHeaderRow().prefHeight(w);
|
||||
|
||||
double prefHeight = topInset + bottomInset + headerHeight;
|
||||
if (fixedCellSize > 0 || itemCount == 0) return prefHeight + fixedCellSize * itemCount;
|
||||
int i = 0;
|
||||
TableRow<S> r;
|
||||
while (i < itemCount && !(r = flow.getCell(i)).isEmpty() && r.getItem() != null) {
|
||||
prefHeight += r.prefHeight(w);
|
||||
i++;
|
||||
}
|
||||
return prefHeight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,7 +73,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
private final GameInstanceManifest manifest;
|
||||
private final String memory;
|
||||
private final String total_memory;
|
||||
private final String java;
|
||||
@@ -91,16 +91,16 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, GameInstanceManifest manifest, LaunchOptions launchOptions, List<Log> logs) {
|
||||
Themes.applyNativeDarkMode(this);
|
||||
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.manifest = manifest;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
this.analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-");
|
||||
|
||||
@@ -142,7 +142,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log");
|
||||
Path latestLog = repository.getRunDirectory(manifest.id()).resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
@@ -291,7 +291,7 @@ public class GameCrashWindow extends Stage {
|
||||
}
|
||||
});
|
||||
|
||||
return LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs,
|
||||
return LogExporter.exportLogs(logFile, repository, launchOptions.getInstanceId(), logs,
|
||||
new CommandBuilder().addAll(managedProcess.getCommands()).toString(),
|
||||
path -> {
|
||||
try {
|
||||
@@ -345,7 +345,7 @@ public class GameCrashWindow extends Stage {
|
||||
TwoLineListItem version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("game.version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
version.setSubtitle(GameCrashWindow.this.manifest.id().toString());
|
||||
|
||||
TwoLineListItem total_memory = new TwoLineListItem();
|
||||
total_memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
|
||||
@@ -17,27 +17,41 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class HTMLRenderer {
|
||||
private static final String INDENT = " ";
|
||||
|
||||
private static URI resolveLink(Node linkNode) {
|
||||
String href = linkNode.absUrl("href");
|
||||
if (href.isEmpty())
|
||||
@@ -50,6 +64,42 @@ public final class HTMLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// @see org.jsoup.internal.StringUtil#isWhitespace(int)
|
||||
public static boolean isWhitespace(int c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
/// @see org.jsoup.internal.StringUtil#isInvisibleChar(int)
|
||||
public static boolean isInvisibleChar(int c) {
|
||||
return c == 8203 || c == 173; // zero width sp, soft hyphen
|
||||
// previously also included zw non join, zw join - but removing those breaks semantic meaning of text
|
||||
}
|
||||
|
||||
/// @see org.jsoup.internal.StringUtil#normaliseWhitespace(String)
|
||||
/// @see org.jsoup.internal.StringUtil#isActuallyWhitespace(int)
|
||||
public static String normaliseWhitespace(String str) {
|
||||
var accum = new StringBuilder();
|
||||
boolean lastWasWhite = false;
|
||||
int len = str.length();
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
int c = str.codePointAt(i);
|
||||
if (isWhitespace(c)) { // Ignore
|
||||
if (lastWasWhite) {
|
||||
i += Character.charCount(c);
|
||||
continue;
|
||||
}
|
||||
accum.append(' ');
|
||||
lastWasWhite = true;
|
||||
} else if (!isInvisibleChar(c)) {
|
||||
accum.appendCodePoint(c);
|
||||
lastWasWhite = false;
|
||||
}
|
||||
i += Character.charCount(c);
|
||||
}
|
||||
return accum.toString();
|
||||
}
|
||||
|
||||
private final List<javafx.scene.Node> children = new ArrayList<>();
|
||||
private final List<Node> stack = new ArrayList<>();
|
||||
|
||||
@@ -58,12 +108,17 @@ public final class HTMLRenderer {
|
||||
private boolean underline;
|
||||
private boolean strike;
|
||||
private boolean highlight;
|
||||
private int indentLevel;
|
||||
private boolean preformatted;
|
||||
private boolean code;
|
||||
private int listDepth;
|
||||
private String headerLevel;
|
||||
private Node hyperlink;
|
||||
private String fxStyle;
|
||||
|
||||
private final Consumer<URI> onClickHyperlink;
|
||||
|
||||
private boolean rendered;
|
||||
|
||||
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
|
||||
this.onClickHyperlink = onClickHyperlink;
|
||||
}
|
||||
@@ -74,42 +129,32 @@ public final class HTMLRenderer {
|
||||
underline = false;
|
||||
strike = false;
|
||||
highlight = false;
|
||||
preformatted = false;
|
||||
code = false;
|
||||
listDepth = 0;
|
||||
headerLevel = null;
|
||||
hyperlink = null;
|
||||
fxStyle = null;
|
||||
|
||||
var declarations = new SimpleCssDeclarations();
|
||||
for (Node node : stack) {
|
||||
String nodeName = node.nodeName();
|
||||
switch (nodeName) {
|
||||
case "b":
|
||||
case "strong":
|
||||
bold = true;
|
||||
break;
|
||||
case "i":
|
||||
case "em":
|
||||
italic = true;
|
||||
break;
|
||||
case "ins":
|
||||
underline = true;
|
||||
break;
|
||||
case "del":
|
||||
strike = true;
|
||||
break;
|
||||
case "mark":
|
||||
highlight = true;
|
||||
break;
|
||||
case "a":
|
||||
hyperlink = node;
|
||||
break;
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
headerLevel = nodeName;
|
||||
break;
|
||||
case "b", "strong" -> bold = true;
|
||||
case "i", "em" -> italic = true;
|
||||
case "ins" -> underline = true;
|
||||
case "del" -> strike = true;
|
||||
case "mark" -> highlight = true;
|
||||
case "pre" -> preformatted = true;
|
||||
case "code" -> code = true;
|
||||
case "a" -> hyperlink = node;
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6" -> headerLevel = nodeName;
|
||||
case "li" -> listDepth++;
|
||||
}
|
||||
|
||||
declarations.add(node.attr("style"));
|
||||
}
|
||||
fxStyle = declarations.asString();
|
||||
}
|
||||
|
||||
private void pushNode(Node node) {
|
||||
@@ -123,6 +168,14 @@ public final class HTMLRenderer {
|
||||
}
|
||||
|
||||
private void applyStyle(Text text) {
|
||||
if (code) {
|
||||
text.getStyleClass().add("html-code");
|
||||
text.setStyle("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT)));
|
||||
return;
|
||||
}
|
||||
|
||||
var styleBuilder = new StringBuilder();
|
||||
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
if (target != null) {
|
||||
@@ -146,12 +199,22 @@ public final class HTMLRenderer {
|
||||
|
||||
if (headerLevel != null)
|
||||
text.getStyleClass().add("html-" + headerLevel);
|
||||
|
||||
if (fxStyle != null)
|
||||
styleBuilder.append(fxStyle);
|
||||
text.setStyle(styleBuilder.toString());
|
||||
}
|
||||
|
||||
private void appendText(String text) {
|
||||
Text textNode = new Text(text);
|
||||
applyStyle(textNode);
|
||||
children.add(textNode);
|
||||
if (code) {
|
||||
var codeFlow = new CodeFlow(textNode);
|
||||
codeFlow.getStyleClass().add("html-code-block");
|
||||
children.add(codeFlow);
|
||||
} else {
|
||||
children.add(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAutoLineBreak(String text) {
|
||||
@@ -185,20 +248,22 @@ public final class HTMLRenderer {
|
||||
}
|
||||
|
||||
try {
|
||||
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
|
||||
.run();
|
||||
if (image == null)
|
||||
throw new AssertionError("Image loading task returned null");
|
||||
ImageView imageView = new ImageView();
|
||||
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(src, width, height, true, true));
|
||||
imageView.setPreserveRatio(true);
|
||||
|
||||
ImagePane imagePane = new ImagePane(imageView, alt);
|
||||
|
||||
ImageView imageView = new ImageView(image);
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
if (target != null) {
|
||||
FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target));
|
||||
imageView.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(imagePane, () -> onClickHyperlink.accept(target));
|
||||
imagePane.setCursor(Cursor.HAND);
|
||||
}
|
||||
}
|
||||
children.add(imageView);
|
||||
|
||||
children.add(imagePane);
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load image: " + src, e);
|
||||
@@ -209,59 +274,203 @@ public final class HTMLRenderer {
|
||||
appendText(alt);
|
||||
}
|
||||
|
||||
public void appendNode(Node node) {
|
||||
if (node instanceof TextNode) {
|
||||
appendText(((TextNode) node).text());
|
||||
private void appendTable(Node table) {
|
||||
var childElements = ((Element) table).children();
|
||||
List<Element> captions = new ArrayList<>();
|
||||
|
||||
List<Element> head = new ArrayList<>();
|
||||
List<List<Element>> body = new ArrayList<>();
|
||||
List<Element> foot = new ArrayList<>();
|
||||
|
||||
boolean hasHead = false;
|
||||
boolean hasBody = childElements.stream().map(Element::nodeName).anyMatch("tbody"::equals);
|
||||
boolean hasFoot = false;
|
||||
int columnCount = 0;
|
||||
for (Element child : childElements) {
|
||||
switch (child.nodeName()) {
|
||||
case "caption" -> captions.add(child);
|
||||
case "thead" -> {
|
||||
if (hasHead) continue;
|
||||
hasHead = true;
|
||||
for (Element e : child.children()) {
|
||||
if (e.nameIs("tr")) {
|
||||
head.clear();
|
||||
head.addAll(
|
||||
e.children().stream()
|
||||
.filter(n -> n.nameIs("th") || n.nameIs("td"))
|
||||
.toList()
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (e.nameIs("th") || e.nameIs("td")) {
|
||||
head.add(e);
|
||||
}
|
||||
}
|
||||
columnCount = Math.max(columnCount, head.size());
|
||||
}
|
||||
case "tbody" -> {
|
||||
var rows = child.children().stream()
|
||||
.filter(e -> e.nameIs("tr"))
|
||||
.map(e ->
|
||||
e.children().stream().filter(n -> n.nameIs("th") || n.nameIs("td")).toList())
|
||||
.filter(row -> !row.isEmpty())
|
||||
.toList();
|
||||
body.addAll(rows);
|
||||
columnCount = Math.max(columnCount, rows.stream().mapToInt(List::size).max().orElse(0));
|
||||
}
|
||||
case "tfoot" -> {
|
||||
if (hasFoot) continue;
|
||||
hasFoot = true;
|
||||
for (Element e : child.children()) {
|
||||
if (e.nameIs("tr")) {
|
||||
foot.clear();
|
||||
foot.addAll(
|
||||
e.children().stream()
|
||||
.filter(n -> n.nameIs("th") || n.nameIs("td"))
|
||||
.toList()
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (e.nameIs("th") || e.nameIs("td")) {
|
||||
foot.add(e);
|
||||
}
|
||||
}
|
||||
columnCount = Math.max(columnCount, foot.size());
|
||||
}
|
||||
case "tr" -> {
|
||||
if (hasBody) continue;
|
||||
List<Element> row = child.children().stream()
|
||||
.filter(n -> n.nameIs("th") || n.nameIs("td"))
|
||||
.toList();
|
||||
if (!row.isEmpty()) {
|
||||
body.add(row);
|
||||
columnCount = Math.max(columnCount, row.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
head = Lang.copyWithSize(head, columnCount, null);
|
||||
|
||||
List<List<Element>> rows = new ArrayList<>(hasFoot ? body.size() + 1 : body.size());
|
||||
for (List<Element> row : body)
|
||||
rows.add(Lang.copyWithSize(row, columnCount, null));
|
||||
if (hasFoot)
|
||||
rows.add(Lang.copyWithSize(foot, columnCount, null));
|
||||
|
||||
TableView<List<Element>> tableView = FXUtils.newAutoHeightTable(FXCollections.observableList(rows));
|
||||
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
for (int i = 0; i < columnCount; i++) {
|
||||
int finalI = i;
|
||||
TableColumn<List<Element>, Element> c = new TableColumn<>();
|
||||
Element e = head.get(i);
|
||||
if (e != null) {
|
||||
var box = new VBox(new HTMLRenderer(onClickHyperlink).appendNode(e).mergeLineBreaks().render());
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
c.setGraphic(box);
|
||||
}
|
||||
c.setCellFactory(param -> new TableCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Element item, boolean empty) {
|
||||
if (item == getItem()) return;
|
||||
super.updateItem(item, empty);
|
||||
if (item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(new Pane(new HTMLRenderer(onClickHyperlink).appendNode(item).mergeLineBreaks().render()));
|
||||
}
|
||||
}
|
||||
});
|
||||
c.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().get(finalI)));
|
||||
tableView.getColumns().add(c);
|
||||
}
|
||||
|
||||
for (Element caption : captions) {
|
||||
appendAutoLineBreak("\n\n");
|
||||
appendChildren(caption);
|
||||
appendAutoLineBreak("\n");
|
||||
}
|
||||
|
||||
children.add(tableView);
|
||||
}
|
||||
|
||||
private void appendOrderedList(Node node) {
|
||||
pushNode(node);
|
||||
int ordinal = StringUtils.toInt(node.attr("start")).orElse(1);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
if (childNode.nameIs("li")) {
|
||||
ordinal = StringUtils.toInt(childNode.attr("value")).orElse(ordinal);
|
||||
appendAutoLineBreak("\n");
|
||||
appendText(" " + " ".repeat(listDepth) + ordinal++ + ". ");
|
||||
appendChildren(childNode);
|
||||
continue;
|
||||
}
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
}
|
||||
|
||||
private void appendChildren(Node node) {
|
||||
if (node.childNodeSize() > 0) {
|
||||
if (node.nameIs("table")) {
|
||||
appendTable(node);
|
||||
} else if (node.nameIs("ol")) {
|
||||
appendOrderedList(node);
|
||||
} else {
|
||||
pushNode(node);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HTMLRenderer appendNode(Node node) {
|
||||
if (node instanceof TextNode n) {
|
||||
appendText(preformatted ? n.getWholeText() : normaliseWhitespace(n.getWholeText()));
|
||||
}
|
||||
|
||||
String name = node.nodeName();
|
||||
switch (name) {
|
||||
case "img":
|
||||
case "img" -> {
|
||||
if (!children.isEmpty())
|
||||
appendAutoLineBreak("\n");
|
||||
appendImage(node);
|
||||
break;
|
||||
case "li":
|
||||
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
|
||||
break;
|
||||
case "dt":
|
||||
appendText(" ");
|
||||
break;
|
||||
case "p":
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
case "tr":
|
||||
appendAutoLineBreak("\n");
|
||||
}
|
||||
case "li" -> {
|
||||
appendAutoLineBreak("\n");
|
||||
appendText(" " + " ".repeat(listDepth) + "\u2022 ");
|
||||
}
|
||||
case "dt" -> appendText(" ");
|
||||
case "p" -> {
|
||||
var n = node.parent();
|
||||
if (!children.isEmpty() && (n == null || !n.nameIs("li")))
|
||||
appendAutoLineBreak("\n\n");
|
||||
}
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6" -> {
|
||||
if (!children.isEmpty())
|
||||
appendAutoLineBreak("\n\n");
|
||||
break;
|
||||
}
|
||||
case "hr" -> {
|
||||
appendAutoLineBreak("\n\n");
|
||||
this.children.add(new Separator());
|
||||
}
|
||||
}
|
||||
|
||||
if (node.childNodeSize() > 0) {
|
||||
boolean isLiNode = "li".equals(name);
|
||||
if (isLiNode) indentLevel++;
|
||||
pushNode(node);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
if (isLiNode) indentLevel--;
|
||||
}
|
||||
appendChildren(node);
|
||||
|
||||
switch (name) {
|
||||
case "br":
|
||||
case "dd":
|
||||
case "p":
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
appendAutoLineBreak("\n");
|
||||
break;
|
||||
case "br", "dd", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n");
|
||||
case "p" -> {
|
||||
var n = node.parent();
|
||||
if (n == null || !n.nameIs("li"))
|
||||
appendAutoLineBreak("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static boolean isSpacing(String text) {
|
||||
@@ -276,7 +485,7 @@ public final class HTMLRenderer {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void mergeLineBreaks() {
|
||||
public HTMLRenderer mergeLineBreaks() {
|
||||
for (int i = 0; i < this.children.size(); i++) {
|
||||
javafx.scene.Node child = this.children.get(i);
|
||||
if (child instanceof AutoLineBreak) {
|
||||
@@ -287,7 +496,7 @@ public final class HTMLRenderer {
|
||||
|
||||
if (otherChild instanceof AutoLineBreak) {
|
||||
lastAutoLineBreak = j;
|
||||
} else if (otherChild instanceof Text && isSpacing(((Text) otherChild).getText())) {
|
||||
} else if (otherChild instanceof Text txt && isSpacing(txt.getText())) {
|
||||
// do nothing
|
||||
} else {
|
||||
break;
|
||||
@@ -303,12 +512,58 @@ public final class HTMLRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Remove empty lines at the beginning
|
||||
int size = children.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
var child = children.get(i);
|
||||
if (child instanceof AutoLineBreak || child instanceof Text txt && isSpacing(txt.getText())) {
|
||||
// NO-OP
|
||||
} else {
|
||||
this.children.subList(0, i).clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Remove empty lines and spaces at the end
|
||||
int size = children.size();
|
||||
for (int i = size - 1; i > -1; i--) {
|
||||
var child = children.get(i);
|
||||
if (child instanceof AutoLineBreak || child instanceof Text txt && isSpacing(txt.getText())) {
|
||||
// NO-OP
|
||||
} else {
|
||||
this.children.subList(i + 1, size).clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextFlow render() {
|
||||
if (rendered) throw new IllegalStateException("Should not render twice");
|
||||
rendered = true;
|
||||
TextFlow textFlow = new TextFlow();
|
||||
textFlow.getStyleClass().add("html");
|
||||
textFlow.getChildren().setAll(children);
|
||||
for (javafx.scene.Node node : children) {
|
||||
if (node instanceof ImagePane imgPane) {
|
||||
ImageView img = imgPane.imageView;
|
||||
InvalidationListener listener = __ -> img.setFitWidth(Math.min(textFlow.getWidth() * 0.9, img.getImage() == null ? 0D : img.getImage().getWidth()));
|
||||
textFlow.widthProperty().addListener(listener);
|
||||
img.imageProperty().addListener(listener);
|
||||
listener.invalidated(null);
|
||||
} else if (node instanceof TableView<?> table) {
|
||||
table.prefWidthProperty().bind(textFlow.widthProperty().multiply(0.8));
|
||||
} else if (node instanceof CodeFlow codeFlow) {
|
||||
codeFlow.maxWidthProperty().bind(textFlow.widthProperty().multiply(0.8));
|
||||
} else if (node instanceof Separator separator) {
|
||||
separator.prefWidthProperty().bind(textFlow.widthProperty().subtract(24));
|
||||
}
|
||||
}
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
@@ -317,4 +572,62 @@ public final class HTMLRenderer {
|
||||
super(text);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ImagePane extends StackPane {
|
||||
|
||||
public final ImageView imageView;
|
||||
|
||||
public ImagePane(ImageView imageView, @Nullable String alt) {
|
||||
super();
|
||||
this.imageView = imageView;
|
||||
|
||||
var txt = new Text(alt);
|
||||
if (StringUtils.isNotBlank(alt)) getChildren().add(txt);
|
||||
getChildren().add(imageView);
|
||||
FXUtils.onChangeAndOperate(imageView.imageProperty(), img -> {
|
||||
if (img != null) getChildren().remove(txt);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class CodeFlow extends TextFlow {
|
||||
|
||||
public CodeFlow(Text text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SimpleCssDeclarations {
|
||||
|
||||
@Unmodifiable
|
||||
private static final Map<String, String> cssPropertyMapping = Map.of(
|
||||
"color", "-fx-fill",
|
||||
"font-size", "-fx-font-size"
|
||||
);
|
||||
|
||||
private final Map<String, String> declarations = new HashMap<>();
|
||||
|
||||
public void add(String inlineCss) {
|
||||
if (StringUtils.isBlank(inlineCss)) return;
|
||||
Arrays.stream(inlineCss.split(";")).filter(StringUtils::isNotBlank).forEach(s -> {
|
||||
String[] declaration = s.split(":", 2);
|
||||
if (declaration.length != 2) return;
|
||||
declaration[0] = declaration[0].trim();
|
||||
declaration[1] = declaration[1].trim();
|
||||
String mappedKey = cssPropertyMapping.get(declaration[0].toLowerCase(Locale.ROOT));
|
||||
if (mappedKey != null) {
|
||||
declarations.put(mappedKey, declaration[1]);
|
||||
} // Ignore unsupported fields
|
||||
});
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
declarations.forEach((property, value) -> sb.append(property).append(": ").append(value).append(";"));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -54,7 +54,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final VersionIconType iconType;
|
||||
private final GameInstanceIconType iconType;
|
||||
private final Style style;
|
||||
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
|
||||
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
|
||||
@@ -92,15 +92,15 @@ public class InstallerItem extends Control {
|
||||
this.style = style;
|
||||
|
||||
iconType = switch (id) {
|
||||
case "game" -> VersionIconType.GRASS;
|
||||
case "fabric", "fabric-api" -> VersionIconType.FABRIC;
|
||||
case "legacyfabric", "legacyfabric-api" -> VersionIconType.LEGACY_FABRIC;
|
||||
case "forge" -> VersionIconType.FORGE;
|
||||
case "cleanroom" -> VersionIconType.CLEANROOM;
|
||||
case "liteloader" -> VersionIconType.CHICKEN;
|
||||
case "optifine" -> VersionIconType.OPTIFINE;
|
||||
case "quilt", "quilt-api" -> VersionIconType.QUILT;
|
||||
case "neoforge" -> VersionIconType.NEO_FORGE;
|
||||
case "game" -> GameInstanceIconType.GRASS;
|
||||
case "fabric", "fabric-api" -> GameInstanceIconType.FABRIC;
|
||||
case "legacyfabric", "legacyfabric-api" -> GameInstanceIconType.LEGACY_FABRIC;
|
||||
case "forge" -> GameInstanceIconType.FORGE;
|
||||
case "cleanroom" -> GameInstanceIconType.CLEANROOM;
|
||||
case "liteloader" -> GameInstanceIconType.CHICKEN;
|
||||
case "optifine" -> GameInstanceIconType.OPTIFINE;
|
||||
case "quilt", "quilt-api" -> GameInstanceIconType.QUILT;
|
||||
case "neoforge" -> GameInstanceIconType.NEO_FORGE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ 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);
|
||||
@@ -174,7 +175,6 @@ 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,11 +331,26 @@ public final class LogWindow extends Stage {
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
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();
|
||||
});
|
||||
}
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
wrapTextProperty().bind(control.wrapText);
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
@@ -395,6 +410,10 @@ 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());
|
||||
|
||||
@@ -414,7 +433,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
control.getGameProcess().getProcess()
|
||||
.onExit()
|
||||
|
||||
@@ -66,6 +66,7 @@ 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"),
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.animation.Timeline;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// A lightweight wrapper for displaying [SVG] icons.
|
||||
@@ -108,7 +109,7 @@ public final class SVGContainer extends Parent {
|
||||
SVG oldIcon = this.icon;
|
||||
this.icon = newIcon;
|
||||
|
||||
if (animationDuration.equals(Duration.ZERO)) {
|
||||
if (animationDuration.equals(Duration.ZERO) || !AnimationUtils.isAnimationEnabled()) {
|
||||
path.setContent(newIcon.getPath());
|
||||
path.setOpacity(1);
|
||||
if (getChildren().size() != 1)
|
||||
|
||||
@@ -22,6 +22,8 @@ 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;
|
||||
@@ -34,8 +36,6 @@ 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.getScene().widthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().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(new URL(url), 30 * 1000);
|
||||
Document document = Jsoup.parse(WebURL.toURL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
|
||||
@@ -47,11 +47,7 @@ public final class WebPage extends SpinnerPane implements DecoratorPage {
|
||||
|
||||
Task.supplyAsync(() -> {
|
||||
Document document = Jsoup.parseBodyFragment(content);
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
|
||||
FXUtils.openLink(uri.toString());
|
||||
}, null);
|
||||
});
|
||||
HTMLRenderer renderer = new HTMLRenderer(Controllers::openUriOrCopy);
|
||||
renderer.appendNode(document);
|
||||
renderer.mergeLineBreaks();
|
||||
return renderer.render();
|
||||
|
||||
@@ -19,10 +19,22 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.windows.IPropertyStore;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Shell32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -55,6 +67,145 @@ public final class WindowsNativeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs permanent window listeners that keep AppUserModel relaunch properties in sync
|
||||
/// with the stage's native HWND lifecycle.
|
||||
///
|
||||
/// JavaFX may destroy and recreate the HWND across `hide()` / `show()` (for example with
|
||||
/// `HIDE_AND_REOPEN`). Properties are therefore applied on every [`WindowEvent#WINDOW_SHOWN`]
|
||||
/// and removed with `VT_EMPTY` on every [`WindowEvent#WINDOW_HIDING`], as required by
|
||||
/// [`SHGetPropertyStoreForWindow`](https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shgetpropertystoreforwindow).
|
||||
///
|
||||
/// Handlers must run on the JavaFX application thread: `IPropertyStore` is a COM interface,
|
||||
/// and the FX thread is already initialized for COM on Windows.
|
||||
///
|
||||
/// @param stage the primary launcher stage
|
||||
public static void installWindowsAppUserModelRelaunchProperties(Stage stage) {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
||||
return;
|
||||
if (!NativeUtils.USE_JNA || Shell32.INSTANCE == null)
|
||||
return;
|
||||
|
||||
@Nullable Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null || !Files.isRegularFile(thisJar) || !"exe".equalsIgnoreCase(FileUtils.getExtension(thisJar)))
|
||||
return;
|
||||
|
||||
String exePath = FileUtils.getAbsolutePath(thisJar);
|
||||
String iconResource = exePath + ",0";
|
||||
String relaunchCommand = '"' + exePath + '"';
|
||||
|
||||
stage.addEventFilter(WindowEvent.WINDOW_SHOWN, event -> {
|
||||
try {
|
||||
applyWindowsAppUserModelRelaunchProperties(stage, relaunchCommand, iconResource);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set AppUserModel relaunch properties", e);
|
||||
}
|
||||
});
|
||||
stage.addEventFilter(WindowEvent.WINDOW_HIDING, event -> {
|
||||
try {
|
||||
clearWindowsAppUserModelRelaunchProperties(stage);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to clear AppUserModel relaunch properties", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Writes AppUserModel relaunch properties for the current native window of `stage`.
|
||||
///
|
||||
/// Successful [`IPropertyStore#SetValue`] calls on a window property store take effect
|
||||
/// immediately; no `Commit` is required or useful.
|
||||
///
|
||||
/// @param stage the stage whose native window receives the properties
|
||||
/// @param relaunchCommand the `PKEY_AppUserModel_RelaunchCommand` value
|
||||
/// @param iconResource the `PKEY_AppUserModel_RelaunchIconResource` value
|
||||
private static void applyWindowsAppUserModelRelaunchProperties(Stage stage, String relaunchCommand, String iconResource) {
|
||||
Shell32 shell32 = Shell32.INSTANCE;
|
||||
if (shell32 == null)
|
||||
return;
|
||||
|
||||
OptionalLong handle = WindowsNativeUtils.getWindowHandle(stage);
|
||||
if (handle.isEmpty() || handle.getAsLong() == WinTypes.HANDLE.INVALID_VALUE) {
|
||||
LOG.warning("Failed to get window handle for AppUserModel relaunch properties");
|
||||
return;
|
||||
}
|
||||
|
||||
try (IPropertyStore store = IPropertyStore.forWindow(shell32, handle.getAsLong())) {
|
||||
if (store == null) {
|
||||
LOG.warning("Failed to call SHGetPropertyStoreForWindow");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Relaunch* first, then AppUserModel.ID last. Window property store values take
|
||||
// effect immediately on SetValue; assigning the ID first can let the shell register the
|
||||
// window before relaunch metadata is complete.
|
||||
if (!setWindowsPropertyStoreString(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchCommand, relaunchCommand)
|
||||
|| !setWindowsPropertyStoreString(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchIconResource, iconResource)
|
||||
|| !setWindowsPropertyStoreString(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchDisplayNameResource, Metadata.FULL_NAME)
|
||||
|| !setWindowsPropertyStoreString(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_ID, Metadata.WINDOWS_APP_USER_MODEL_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Set AppUserModel relaunch properties for HWND " + Long.toHexString(handle.getAsLong()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes AppUserModel relaunch properties from the current native window before it is hidden.
|
||||
///
|
||||
/// @param stage the stage whose native window is about to be destroyed or hidden
|
||||
private static void clearWindowsAppUserModelRelaunchProperties(Stage stage) {
|
||||
Shell32 shell32 = Shell32.INSTANCE;
|
||||
if (shell32 == null)
|
||||
return;
|
||||
|
||||
OptionalLong handle = WindowsNativeUtils.getWindowHandle(stage);
|
||||
if (handle.isEmpty() || handle.getAsLong() == WinTypes.HANDLE.INVALID_VALUE) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (IPropertyStore store = IPropertyStore.forWindow(shell32, handle.getAsLong())) {
|
||||
if (store == null) {
|
||||
LOG.warning("Failed to call SHGetPropertyStoreForWindow while clearing properties");
|
||||
return;
|
||||
}
|
||||
|
||||
WinTypes.PROPVARIANT empty = new WinTypes.PROPVARIANT();
|
||||
empty.setEmpty();
|
||||
// Clear in reverse of apply order so ID is removed before relaunch metadata when possible.
|
||||
clearWindowsPropertyStoreValue(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_ID, empty);
|
||||
clearWindowsPropertyStoreValue(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchDisplayNameResource, empty);
|
||||
clearWindowsPropertyStoreValue(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchIconResource, empty);
|
||||
clearWindowsPropertyStoreValue(store, WinTypes.PROPERTYKEY.PKEY_AppUserModel_RelaunchCommand, empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a string property on an [`IPropertyStore`].
|
||||
///
|
||||
/// @param store the property store
|
||||
/// @param key the property key
|
||||
/// @param value the string value
|
||||
/// @return `true` if [`IPropertyStore#SetValue`] succeeds
|
||||
private static boolean setWindowsPropertyStoreString(IPropertyStore store, WinTypes.PROPERTYKEY key, String value) {
|
||||
WinTypes.PROPVARIANT propvar = new WinTypes.PROPVARIANT();
|
||||
propvar.setStringValue(value);
|
||||
int hr = store.SetValue(key, propvar);
|
||||
if (hr < 0) {
|
||||
LOG.warning("Failed to set property pid=" + key.pid + " on IPropertyStore, HRESULT=0x" + Integer.toHexString(hr));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Removes a property by writing `VT_EMPTY`.
|
||||
///
|
||||
/// @param store the property store
|
||||
/// @param key the property key to clear
|
||||
/// @param empty a `VT_EMPTY` value
|
||||
private static void clearWindowsPropertyStoreValue(IPropertyStore store, WinTypes.PROPERTYKEY key, WinTypes.PROPVARIANT empty) {
|
||||
int hr = store.SetValue(key, empty);
|
||||
if (hr < 0) {
|
||||
LOG.warning("Failed to clear property pid=" + key.pid + " on IPropertyStore, HRESULT=0x" + Integer.toHexString(hr));
|
||||
}
|
||||
}
|
||||
|
||||
private WindowsNativeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ 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;
|
||||
@@ -151,7 +150,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 = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class AnimationUtils {
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static final boolean ENABLED = !settings().animationDisabledProperty().get();
|
||||
private static final boolean ENABLED = !settings().isAnimationDisabled();
|
||||
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 = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
Path file = Controllers.showOpenDialog(chooser);
|
||||
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 = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
Path dir = Controllers.showDialog(chooser);
|
||||
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.Stage;
|
||||
import javafx.stage.Window;
|
||||
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,7 +57,9 @@ public class LineFileChooserButton extends LineButton {
|
||||
public void fire() {
|
||||
super.fire();
|
||||
|
||||
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
|
||||
Scene scene = this.getScene();
|
||||
|
||||
Window owner = scene != null ? scene.getWindow() : null; // 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,11 +21,12 @@ 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.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -67,16 +68,18 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
center.getChildren().setAll(lblTitle, taskListPane);
|
||||
}
|
||||
|
||||
BorderPane bottom = new BorderPane();
|
||||
this.setBottom(bottom);
|
||||
HBox bottom = new HBox();
|
||||
bottom.setAlignment(Pos.CENTER_LEFT);
|
||||
bottom.setPadding(new Insets(0, 8, 8, 8));
|
||||
bottom.setSpacing(8);
|
||||
this.setBottom(bottom);
|
||||
{
|
||||
lblProgress = new Label();
|
||||
bottom.setLeft(lblProgress);
|
||||
|
||||
Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
bottom.setRight(btnCancel);
|
||||
bottom.getChildren().setAll(lblProgress, spacer, btnCancel);
|
||||
}
|
||||
|
||||
setCancel(cancel);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import com.jfoenix.controls.JFXSnackbarLayout;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogUtils;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
|
||||
public class DecoratorController {
|
||||
private final Decorator decorator;
|
||||
private final Navigator navigator;
|
||||
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
decorator = new Decorator(stage);
|
||||
decorator.titleTransparentProperty().bind(Themes.titleBarTransparentProperty());
|
||||
|
||||
navigator = new Navigator();
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
navigator.init(mainPage);
|
||||
|
||||
decorator.getContent().setAll(navigator);
|
||||
decorator.onCloseNavButtonActionProperty().set(e -> close());
|
||||
decorator.onBackNavButtonActionProperty().set(e -> back());
|
||||
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
|
||||
|
||||
setupAuthlibInjectorDnD();
|
||||
|
||||
decorator.contentBackgroundProperty().bind(Themes.backgroundProperty());
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
if (!(e.getTarget() instanceof Node t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Node newTarget;
|
||||
|
||||
JFXDialogPane currentDialogPane = null;
|
||||
if (decorator.getDrawerWrapper() != null) {
|
||||
currentDialogPane = (JFXDialogPane) decorator.getDrawerWrapper().getProperties().get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
}
|
||||
|
||||
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
|
||||
newTarget = currentDialogPane.peek().get();
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage();
|
||||
}
|
||||
boolean needsRedirect = true;
|
||||
|
||||
while (t != null) {
|
||||
if (t == newTarget) {
|
||||
// current event target is in newTarget
|
||||
needsRedirect = false;
|
||||
break;
|
||||
}
|
||||
t = t.getParent();
|
||||
}
|
||||
if (!needsRedirect) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.consume();
|
||||
newTarget.fireEvent(e.copyFor(e.getSource(), newTarget));
|
||||
});
|
||||
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.BACK) {
|
||||
back();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Decorator getDecorator() {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
// ==== Navigation ====
|
||||
|
||||
public void navigate(Node node, AnimationProducer animationProducer, Duration duration, Interpolator interpolator) {
|
||||
navigator.navigate(node, animationProducer, duration, interpolator);
|
||||
}
|
||||
|
||||
public BooleanProperty backableProperty() {
|
||||
return navigator.backableProperty();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (navigator.getCurrentPage() instanceof DecoratorPage) {
|
||||
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
|
||||
|
||||
if (page.isPageCloseable()) {
|
||||
page.closePage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
navigator.clear();
|
||||
}
|
||||
|
||||
private void back() {
|
||||
if (navigator.getCurrentPage() instanceof DecoratorPage) {
|
||||
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
|
||||
|
||||
if (page.back()) {
|
||||
if (navigator.canGoBack()) {
|
||||
navigator.close();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (navigator.canGoBack()) {
|
||||
navigator.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
if (navigator.getCurrentPage() instanceof Refreshable) {
|
||||
Refreshable refreshable = (Refreshable) navigator.getCurrentPage();
|
||||
|
||||
if (refreshable.refreshableProperty().get())
|
||||
refreshable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (event.getSource() != this.navigator) return;
|
||||
Node to = event.getNode();
|
||||
|
||||
if (to instanceof Refreshable) {
|
||||
decorator.canRefreshProperty().bind(((Refreshable) to).refreshableProperty());
|
||||
} else {
|
||||
decorator.canRefreshProperty().unbind();
|
||||
decorator.canRefreshProperty().set(false);
|
||||
}
|
||||
|
||||
decorator.canCloseProperty().set(navigator.size() > 2);
|
||||
|
||||
if (to instanceof DecoratorPage) {
|
||||
decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable());
|
||||
} else {
|
||||
decorator.showCloseAsHomeProperty().set(true);
|
||||
}
|
||||
|
||||
decorator.setNavigationDirection(event.getDirection());
|
||||
|
||||
// state property should be updated at last.
|
||||
if (to instanceof DecoratorPage) {
|
||||
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
|
||||
} else {
|
||||
decorator.stateProperty().unbind();
|
||||
decorator.stateProperty().set(new DecoratorPage.State("", null, navigator.canGoBack(), false, true));
|
||||
}
|
||||
|
||||
if (to instanceof Region) {
|
||||
Region region = (Region) to;
|
||||
// Let root pane fix window size.
|
||||
StackPane parent = (StackPane) region.getParent();
|
||||
region.prefWidthProperty().bind(parent.widthProperty());
|
||||
region.prefHeightProperty().bind(parent.heightProperty());
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Dialog ====
|
||||
public void showDialog(Node node) {
|
||||
DialogUtils.show(decorator, node);
|
||||
}
|
||||
|
||||
private void closeDialog(Node node) {
|
||||
DialogUtils.close(node);
|
||||
}
|
||||
|
||||
public void showDialogLater(Node node) {
|
||||
DialogUtils.showLater(decorator, node);
|
||||
}
|
||||
|
||||
// ==== Toast ====
|
||||
|
||||
public void showToast(String content) {
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content)));
|
||||
}
|
||||
|
||||
// ==== Wizard ====
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider) {
|
||||
startWizard(wizardProvider, null);
|
||||
}
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider, String category) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category),
|
||||
ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
// ==== Authlib Injector DnD ====
|
||||
|
||||
private void setupAuthlibInjectorDnD() {
|
||||
decorator.addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler());
|
||||
decorator.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler(
|
||||
url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url))));
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane root, parent;
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
private final EventHandler<MouseEvent> onTitleBarDoubleClick;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
public DecoratorSkin(Decorator control) {
|
||||
super(control);
|
||||
|
||||
primaryStage = control.getPrimaryStage();
|
||||
|
||||
Decorator skinnable = getSkinnable();
|
||||
root = new StackPane();
|
||||
root.getStyleClass().add("window");
|
||||
|
||||
StackPane shadowContainer = new StackPane();
|
||||
shadowContainer.getStyleClass().add("body");
|
||||
shadowContainer.setEffect(new DropShadow(BlurType.ONE_PASS_BOX, Color.rgb(0, 0, 0, 0.4), 10, 0.3, 0.0, 0.0));
|
||||
|
||||
parent = new StackPane();
|
||||
Rectangle clip = new Rectangle();
|
||||
clip.widthProperty().bind(parent.widthProperty());
|
||||
clip.heightProperty().bind(parent.heightProperty());
|
||||
clip.setArcWidth(8);
|
||||
clip.setArcHeight(8);
|
||||
parent.setClip(clip);
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
onTitleBarDoubleClick = event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
} else {
|
||||
onWindowsStatusChange = null;
|
||||
onTitleBarDoubleClick = null;
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
|
||||
StackPane wrapper = new StackPane();
|
||||
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? null
|
||||
: skinnable.getContentBackground().background(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? 1.0
|
||||
: skinnable.getContentBackground().opacity(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
|
||||
BorderPane frame = new BorderPane();
|
||||
frame.getStyleClass().addAll("jfx-decorator");
|
||||
wrapper.getChildren().setAll(backgroundNode, frame);
|
||||
skinnable.setDrawerWrapper(wrapper);
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
|
||||
// center node with an animation layer at bottom, a container layer at middle and a "welcome" layer at top.
|
||||
StackPane container = new StackPane();
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// content layer at middle
|
||||
{
|
||||
StackPane contentPlaceHolder = new StackPane();
|
||||
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
|
||||
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
|
||||
|
||||
container.getChildren().add(contentPlaceHolder);
|
||||
}
|
||||
|
||||
// welcome and hint layer at top
|
||||
{
|
||||
StackPane floatLayer = new StackPane();
|
||||
Bindings.bindContent(floatLayer.getChildren(), skinnable.containerProperty());
|
||||
ListChangeListener<Node> listener = c -> {
|
||||
if (skinnable.getContainer().isEmpty()) {
|
||||
floatLayer.setMouseTransparent(true);
|
||||
floatLayer.setVisible(false);
|
||||
} else {
|
||||
floatLayer.setMouseTransparent(false);
|
||||
floatLayer.setVisible(true);
|
||||
}
|
||||
};
|
||||
skinnable.containerProperty().addListener(listener);
|
||||
listener.onChanged(null);
|
||||
|
||||
container.getChildren().add(floatLayer);
|
||||
}
|
||||
|
||||
frame.setCenter(container);
|
||||
|
||||
titleContainer = new StackPane();
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.max(0.0, wrapper.getHeight()
|
||||
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
|
||||
wrapper.heightProperty(),
|
||||
skinnable.titleTransparentProperty(),
|
||||
titleContainer.heightProperty()));
|
||||
|
||||
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
|
||||
// and decide whether the whole top bar should be rendered in white or black. TODO
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
} else {
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
}
|
||||
});
|
||||
|
||||
control.capableDraggingWindow(titleContainer);
|
||||
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleContainer.getChildren().add(titleBar);
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.leftPaneWidth(), s.backable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.refreshable(), s.title(), s.titleNode());
|
||||
if (s.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
default -> ContainerAnimations.FADE;
|
||||
};
|
||||
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
|
||||
navBarPane.setContent(node, animation, Motion.SHORT4);
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
}
|
||||
frame.setTop(titleContainer);
|
||||
|
||||
{
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setAlignment(Pos.TOP_RIGHT);
|
||||
buttonsContainer.setMaxHeight(40);
|
||||
{
|
||||
JFXButton btnHelp = new JFXButton();
|
||||
btnHelp.setFocusTraversable(false);
|
||||
btnHelp.setGraphic(SVG.HELP.createIcon(Themes.titleFillProperty()));
|
||||
btnHelp.getStyleClass().add("jfx-decorator-button");
|
||||
btnHelp.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
btnMin.setFocusTraversable(false);
|
||||
btnMin.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.getStyleClass().add("jfx-decorator-button");
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
JFXButton btnClose = new JFXButton();
|
||||
btnClose.setFocusTraversable(false);
|
||||
btnClose.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
btnClose.getStyleClass().add("jfx-decorator-button");
|
||||
btnClose.setOnAction(e -> skinnable.close());
|
||||
|
||||
buttonsContainer.getChildren().setAll(btnHelp, btnMin, btnClose);
|
||||
}
|
||||
AnchorPane layer = new AnchorPane();
|
||||
layer.setPickOnBounds(false);
|
||||
layer.getChildren().add(buttonsContainer);
|
||||
AnchorPane.setTopAnchor(buttonsContainer, 0.0);
|
||||
AnchorPane.setRightAnchor(buttonsContainer, 0.0);
|
||||
buttonsContainerPlaceHolder.widthProperty().bind(buttonsContainer.widthProperty());
|
||||
parent.getChildren().add(layer);
|
||||
}
|
||||
|
||||
getChildren().add(root);
|
||||
}
|
||||
|
||||
private Node createNavBar(Decorator skinnable, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
|
||||
BorderPane navBar = new BorderPane();
|
||||
navBar.getStyleClass().add("navigation-bar");
|
||||
|
||||
{
|
||||
HBox navLeft = new HBox();
|
||||
navLeft.setAlignment(Pos.CENTER_LEFT);
|
||||
navLeft.setPadding(new Insets(0, 5, 0, 5));
|
||||
|
||||
if (canBack) {
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(backNavButton);
|
||||
backNavButton.setFocusTraversable(false);
|
||||
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().set(canBack);
|
||||
|
||||
navLeft.getChildren().add(backNavButton);
|
||||
}
|
||||
|
||||
if (canClose) {
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(closeNavButton);
|
||||
closeNavButton.setFocusTraversable(false);
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
if (showCloseAsHome)
|
||||
closeNavButton.setGraphic(SVG.HOME.createIcon(Themes.titleFillProperty()));
|
||||
else
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
|
||||
navLeft.getChildren().add(closeNavButton);
|
||||
}
|
||||
|
||||
if (canBack || canClose) {
|
||||
navBar.setLeft(navLeft);
|
||||
}
|
||||
|
||||
BorderPane center = new BorderPane();
|
||||
if (title != null) {
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
if (titleNode == null) {
|
||||
titleLabel.maxWidthProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getWidth() - 150 - navLeft.getWidth(),
|
||||
skinnable.widthProperty(), navLeft.widthProperty()));
|
||||
} else {
|
||||
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
// 8 (margin-left)
|
||||
return leftPaneWidth - 8 - navLeft.getWidth();
|
||||
}, navLeft.widthProperty()));
|
||||
}
|
||||
titleLabel.setText(title);
|
||||
center.setLeft(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
}
|
||||
if (titleNode != null) {
|
||||
center.setCenter(titleNode);
|
||||
BorderPane.setAlignment(titleNode, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(titleNode, new Insets(0, 0, 0, 8));
|
||||
}
|
||||
if (onTitleBarDoubleClick != null)
|
||||
center.setOnMouseClicked(onTitleBarDoubleClick);
|
||||
center.setOnMouseDragged(mouseEvent -> {
|
||||
if (!getSkinnable().isDragging() && primaryStage.isMaximized()) {
|
||||
getSkinnable().setDragging(true);
|
||||
mouseInitX = mouseEvent.getScreenX();
|
||||
mouseInitY = mouseEvent.getScreenY();
|
||||
primaryStage.setMaximized(false);
|
||||
stageInitWidth = primaryStage.getWidth();
|
||||
stageInitHeight = primaryStage.getHeight();
|
||||
primaryStage.setY(stageInitY = 0);
|
||||
primaryStage.setX(stageInitX = mouseInitX - stageInitWidth / 2);
|
||||
}
|
||||
});
|
||||
navBar.setCenter(center);
|
||||
|
||||
if (canRefresh) {
|
||||
HBox navRight = new HBox();
|
||||
navRight.setAlignment(Pos.CENTER_RIGHT);
|
||||
JFXButton refreshNavButton = new JFXButton();
|
||||
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
skinnable.forbidDraggingWindow(refreshNavButton);
|
||||
|
||||
navRight.getChildren().setAll(refreshNavButton);
|
||||
navBar.setRight(navRight);
|
||||
}
|
||||
}
|
||||
return navBar;
|
||||
}
|
||||
|
||||
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x < root.getWidth() && x >= root.getWidth() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y >= 0 && y <= root.snappedTopInset();
|
||||
}
|
||||
|
||||
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y < root.getHeight() && y >= root.getHeight() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x >= 0 && x <= root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private void resizeStage(double newWidth, double newHeight) {
|
||||
if (newWidth < 0)
|
||||
newWidth = primaryStage.getWidth();
|
||||
if (newWidth < primaryStage.getMinWidth())
|
||||
newWidth = primaryStage.getMinWidth();
|
||||
if (newWidth < titleContainer.getMinWidth())
|
||||
newWidth = titleContainer.getMinWidth();
|
||||
|
||||
if (newHeight < 0)
|
||||
newHeight = primaryStage.getHeight();
|
||||
if (newHeight < primaryStage.getMinHeight())
|
||||
newHeight = primaryStage.getMinHeight();
|
||||
if (newHeight < titleContainer.getMinHeight())
|
||||
newHeight = titleContainer.getMinHeight();
|
||||
|
||||
// Width and height must be set simultaneously to avoid JDK-8344372 (https://github.com/openjdk/jfx/pull/1654)
|
||||
primaryStage.setWidth(newWidth);
|
||||
primaryStage.setHeight(newHeight);
|
||||
}
|
||||
|
||||
private void onMouseMoved(MouseEvent mouseEvent) {
|
||||
if (!primaryStage.isFullScreen() && primaryStage.isResizable()) {
|
||||
double x = mouseEvent.getX(), y = mouseEvent.getY();
|
||||
Bounds boundsInParent = root.getBoundsInParent();
|
||||
double diagonalSize = root.snappedLeftInset() + 10;
|
||||
if (this.isRightEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.E_RESIZE);
|
||||
}
|
||||
} else if (this.isLeftEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.W_RESIZE);
|
||||
}
|
||||
} else if (this.isTopEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.N_RESIZE);
|
||||
}
|
||||
} else if (this.isBottomEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.S_RESIZE);
|
||||
}
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMouseReleased(MouseEvent mouseEvent) {
|
||||
getSkinnable().setDragging(false);
|
||||
}
|
||||
|
||||
private void onMouseDragged(MouseEvent mouseEvent) {
|
||||
if (!getSkinnable().isDragging()) {
|
||||
getSkinnable().setDragging(true);
|
||||
mouseInitX = mouseEvent.getScreenX();
|
||||
mouseInitY = mouseEvent.getScreenY();
|
||||
stageInitX = primaryStage.getX();
|
||||
stageInitY = primaryStage.getY();
|
||||
stageInitWidth = primaryStage.getWidth();
|
||||
stageInitHeight = primaryStage.getHeight();
|
||||
}
|
||||
|
||||
if (primaryStage.isFullScreen() || !mouseEvent.isPrimaryButtonDown() || mouseEvent.isStillSincePress())
|
||||
return;
|
||||
|
||||
double dx = mouseEvent.getScreenX() - mouseInitX;
|
||||
double dy = mouseEvent.getScreenY() - mouseInitY;
|
||||
|
||||
Cursor cursor = root.getCursor();
|
||||
if (getSkinnable().isAllowMove()) {
|
||||
if (cursor == Cursor.DEFAULT) {
|
||||
primaryStage.setX(stageInitX + dx);
|
||||
primaryStage.setY(stageInitY + dy);
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
|
||||
if (getSkinnable().isResizable()) {
|
||||
if (cursor == Cursor.E_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, -1);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.S_RESIZE) {
|
||||
resizeStage(-1, stageInitHeight + dy);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.W_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, -1);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.N_RESIZE) {
|
||||
resizeStage(-1, stageInitHeight - dy);
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.SE_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, stageInitHeight + dy);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.SW_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, stageInitHeight + dy);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.NW_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, stageInitHeight - dy);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.NE_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, stageInitHeight - dy);
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NavBarAnimations implements TransitionPane.AnimationProducer {
|
||||
NEXT {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return NEXT;
|
||||
}
|
||||
},
|
||||
|
||||
PREVIOUS {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(Pane container, Node previousNode, Node nextNode, Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return PREVIOUS;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Renders the clipped background, title bar, and navigation content owned by a [Decorator].
|
||||
@NotNullByDefault
|
||||
final class MainWindowPane extends StackPane {
|
||||
/// The diameter, in pixels, of the rounded window corners.
|
||||
private static final double ARC = 8.0;
|
||||
|
||||
/// The decorator whose state and actions are represented by this pane.
|
||||
private final Decorator decorator;
|
||||
|
||||
/// The clip that rounds normal window corners and becomes square while the window fills the screen.
|
||||
private final Rectangle clip = new Rectangle();
|
||||
|
||||
/// The frame containing the title bar and current navigation page.
|
||||
private final BorderPane frame;
|
||||
|
||||
/// The title bar containing page navigation state.
|
||||
private final BorderPane titleBar;
|
||||
|
||||
/// The transition container used when the title-bar state changes.
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
/// Retains listener delegates that are registered through weak listener wrappers.
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/// Creates and wires the main-window content for `decorator`.
|
||||
///
|
||||
/// @param decorator the owning window decorator
|
||||
MainWindowPane(Decorator decorator) {
|
||||
this.decorator = decorator;
|
||||
|
||||
clip.widthProperty().bind(widthProperty());
|
||||
clip.heightProperty().bind(heightProperty());
|
||||
clip.setArcWidth(ARC);
|
||||
clip.setArcHeight(ARC);
|
||||
setClip(clip);
|
||||
|
||||
backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = createBackgroundNode();
|
||||
|
||||
frame = new BorderPane();
|
||||
frame.getStyleClass().add("jfx-decorator");
|
||||
|
||||
StackPane center = new StackPane();
|
||||
center.getStyleClass().add("jfx-decorator-content-container");
|
||||
FXUtils.setOverflowHidden(center);
|
||||
center.getChildren().setAll(decorator.getNavigator());
|
||||
frame.setCenter(center);
|
||||
|
||||
HBox rightButtonsContainer = createWindowButtons();
|
||||
titleBar = new BorderPane();
|
||||
titleBar.setPickOnBounds(false);
|
||||
titleBar.getStyleClass().add("jfx-tool-bar");
|
||||
titleBar.setRight(rightButtonsContainer);
|
||||
|
||||
navBarPane = new TransitionPane();
|
||||
titleBar.setCenter(navBarPane);
|
||||
frame.setTop(titleBar);
|
||||
|
||||
updateTitleBarBackground();
|
||||
InvalidationListener titleBarBackgroundListener = observable -> updateTitleBarBackground();
|
||||
Themes.colorSchemeProperty().addListener(holder.weak(titleBarBackgroundListener));
|
||||
Themes.titleBarTransparentProperty().addListener(holder.weak(titleBarBackgroundListener));
|
||||
|
||||
InvalidationListener navBarListener = observable -> updateNavBar();
|
||||
decorator.stateProperty().addListener(holder.weak(navBarListener));
|
||||
updateNavBar();
|
||||
|
||||
decorator.capableDraggingWindow(titleBar);
|
||||
|
||||
getChildren().setAll(backgroundNode, frame);
|
||||
}
|
||||
|
||||
/// Updates the content-corner shape for an edge-to-edge window state.
|
||||
///
|
||||
/// @param edgeToEdge whether the attached window is maximized or full-screen
|
||||
void setWindowEdgeToEdge(boolean edgeToEdge) {
|
||||
double arc = edgeToEdge ? 0.0 : ARC;
|
||||
clip.setArcWidth(arc);
|
||||
clip.setArcHeight(arc);
|
||||
}
|
||||
|
||||
/// Creates the launcher-background layer and keeps it synchronized with the active theme.
|
||||
///
|
||||
/// @return the mouse-transparent background node
|
||||
private Region createBackgroundNode() {
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
holder.onWeakChangeAndOperate(Themes.backgroundProperty(), background -> {
|
||||
double opacity = background.opacity();
|
||||
if (opacity >= 0.001) {
|
||||
backgroundNode.setBackground(background.background());
|
||||
backgroundNode.setOpacity(opacity);
|
||||
} else {
|
||||
backgroundNode.setBackground(null);
|
||||
backgroundNode.setOpacity(0);
|
||||
}
|
||||
});
|
||||
return backgroundNode;
|
||||
}
|
||||
|
||||
/// Creates the help, minimize, and application-close buttons.
|
||||
///
|
||||
/// @return a size-limited top-right button row
|
||||
private HBox createWindowButtons() {
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.TOP_RIGHT);
|
||||
buttons.setMaxSize(Region.USE_PREF_SIZE, 40);
|
||||
|
||||
JFXButton helpButton = new JFXButton();
|
||||
helpButton.setFocusTraversable(false);
|
||||
helpButton.setGraphic(SVG.HELP.createIcon(Themes.titleFillProperty()));
|
||||
helpButton.getStyleClass().add("jfx-decorator-button");
|
||||
helpButton.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
decorator.forbidDraggingWindow(helpButton);
|
||||
|
||||
JFXButton minimizeButton = new JFXButton();
|
||||
minimizeButton.setFocusTraversable(false);
|
||||
minimizeButton.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
minimizeButton.getStyleClass().add("jfx-decorator-button");
|
||||
minimizeButton.setOnAction(event -> decorator.minimizeWindow());
|
||||
decorator.forbidDraggingWindow(minimizeButton);
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setFocusTraversable(false);
|
||||
closeButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeButton.setOnAction(event -> decorator.closeWindow());
|
||||
decorator.forbidDraggingWindow(closeButton);
|
||||
|
||||
buttons.getChildren().setAll(helpButton, minimizeButton, closeButton);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/// Recomputes the title-bar background for the current theme and transparency preference.
|
||||
private void updateTitleBarBackground() {
|
||||
@Nullable ColorScheme colorScheme = Themes.getColorScheme();
|
||||
if (colorScheme == null) {
|
||||
titleBar.setBackground(null);
|
||||
} else if (Themes.titleBarTransparentProperty().get()) {
|
||||
Color surface = colorScheme.getSurface();
|
||||
titleBar.setBackground(new Background(new BackgroundFill(
|
||||
new Color(surface.getRed(), surface.getGreen(), surface.getBlue(), 0.5),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
} else {
|
||||
titleBar.setBackground(new Background(new BackgroundFill(
|
||||
colorScheme.getPrimaryContainer(),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the navigation bar from the current page state.
|
||||
private void updateNavBar() {
|
||||
DecoratorPage.@Nullable State state = decorator.getState();
|
||||
if (state == null) {
|
||||
navBarPane.getChildren().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Node navBar = createNavBar(state);
|
||||
|
||||
if (state.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (decorator.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
default -> ContainerAnimations.FADE;
|
||||
};
|
||||
decorator.setNavigationDirection(Navigation.NavigationDirection.START);
|
||||
navBarPane.setContent(navBar, animation, Motion.SHORT4);
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(navBar);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the page-specific navigation controls and title.
|
||||
///
|
||||
/// @param state the current page state
|
||||
/// @return a newly constructed navigation bar
|
||||
private Node createNavBar(DecoratorPage.State state) {
|
||||
HBox navBar = new HBox();
|
||||
navBar.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// Left navigation buttons
|
||||
if (state.backable()) {
|
||||
JFXButton backButton = new JFXButton();
|
||||
backButton.setFocusTraversable(false);
|
||||
backButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backButton.getStyleClass().add("jfx-decorator-button");
|
||||
backButton.setOnAction(event -> decorator.back());
|
||||
decorator.forbidDraggingWindow(backButton);
|
||||
navBar.getChildren().add(backButton);
|
||||
}
|
||||
|
||||
if (decorator.canCloseProperty().get()) {
|
||||
boolean showCloseAsHome = decorator.showCloseAsHomeProperty().get();
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setFocusTraversable(false);
|
||||
closeButton.setGraphic((showCloseAsHome ? SVG.HOME : SVG.CLOSE).createIcon(Themes.titleFillProperty()));
|
||||
closeButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeButton.setOnAction(event -> decorator.closeCurrentPage());
|
||||
decorator.forbidDraggingWindow(closeButton);
|
||||
navBar.getChildren().add(closeButton);
|
||||
}
|
||||
|
||||
// Center title area
|
||||
StackPane titleArea = new StackPane();
|
||||
titleArea.setAlignment(Pos.CENTER_LEFT);
|
||||
titleArea.setMinWidth(0);
|
||||
HBox.setHgrow(titleArea, Priority.ALWAYS);
|
||||
if (state.titleNode() != null) {
|
||||
titleArea.getChildren().setAll(state.titleNode());
|
||||
StackPane.setMargin(state.titleNode(), new Insets(0, 0, 0, 8));
|
||||
} else if (state.title() != null) {
|
||||
Label titleLabel = new Label(state.title());
|
||||
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleLabel.setMinWidth(0);
|
||||
titleArea.getChildren().setAll(titleLabel);
|
||||
}
|
||||
|
||||
decorator.registerTitleBar(titleArea);
|
||||
|
||||
if (!navBar.getChildren().isEmpty()) {
|
||||
Insets padding = new Insets(0, 0, 0, 5);
|
||||
navBar.setPadding(padding);
|
||||
titleArea.setPadding(padding);
|
||||
}
|
||||
|
||||
navBar.getChildren().add(titleArea);
|
||||
|
||||
// Right refresh button
|
||||
if (state.refreshable()) {
|
||||
JFXButton refreshButton = new JFXButton();
|
||||
refreshButton.setFocusTraversable(false);
|
||||
refreshButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshButton.setOnAction(event -> decorator.refresh());
|
||||
decorator.forbidDraggingWindow(refreshButton);
|
||||
|
||||
navBar.getChildren().add(refreshButton);
|
||||
}
|
||||
|
||||
return navBar;
|
||||
}
|
||||
|
||||
/// Produces directional transitions for page-title changes.
|
||||
private enum NavBarAnimations implements TransitionPane.AnimationProducer {
|
||||
/// Moves the next title in from the right.
|
||||
NEXT {
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container,
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return createTimeline(previousNode, nextNode, 50, -50, duration, interpolator);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return PREVIOUS;
|
||||
}
|
||||
},
|
||||
|
||||
/// Moves the previous title in from the left.
|
||||
PREVIOUS {
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(-container.getWidth());
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container,
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return createTimeline(previousNode, nextNode, -50, 50, duration, interpolator);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return NEXT;
|
||||
}
|
||||
};
|
||||
|
||||
/// Creates a title transition with the supplied horizontal offsets.
|
||||
///
|
||||
/// @param previousNode the title being removed
|
||||
/// @param nextNode the title being shown
|
||||
/// @param nextOffset the initial offset of the next title
|
||||
/// @param previousOffset the final offset of the previous title
|
||||
/// @param duration the transition duration
|
||||
/// @param interpolator the transition interpolator
|
||||
/// @return the unstarted transition timeline
|
||||
private static Timeline createTimeline(
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
double nextOffset,
|
||||
double previousOffset,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), nextOffset, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), previousOffset, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,14 @@ 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;
|
||||
@@ -48,18 +49,23 @@ 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,11 +27,15 @@ 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.game.GameInstanceID;
|
||||
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;
|
||||
@@ -42,6 +46,8 @@ import static org.jackhuang.hmcl.setting.SettingsManager.state;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public abstract class AbstractInstallersPage extends Control implements WizardPage {
|
||||
public static final SettingsMap.Key<GameInstanceID> INSTANCE_ID = new SettingsMap.Key<>("instanceId");
|
||||
|
||||
public static final String FABRIC_QUILT_API_TIP = "fabricQuiltApi";
|
||||
protected final WizardController controller;
|
||||
|
||||
@@ -112,6 +118,14 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
return new InstallersPageSkin(this);
|
||||
}
|
||||
|
||||
/// Returns whether the name field includes clear and reset controls.
|
||||
///
|
||||
/// @return `true` to display the controls; `false` otherwise
|
||||
protected abstract boolean showExtendPane();
|
||||
|
||||
/// Restores the default value of the name field.
|
||||
protected abstract void resetDefaultName();
|
||||
|
||||
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
@@ -127,11 +141,27 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
{
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.getStyleClass().add("card-non-transparent");
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
versionNamePane.setStyle("-fx-padding: 20 16 20 16");
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
|
||||
HBox.setHgrow(control.txtName, Priority.ALWAYS);
|
||||
|
||||
versionNamePane.getChildren().addAll(new Label(i18n("instance.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);
|
||||
}
|
||||
|
||||
root.setTop(versionNamePane);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,8 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -38,17 +37,17 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
protected final BooleanProperty compatible = new SimpleBooleanProperty();
|
||||
protected final GameRepository repository;
|
||||
protected final HMCLGameRepository repository;
|
||||
protected final String gameVersion;
|
||||
protected final Version version;
|
||||
protected final GameInstanceManifest manifest;
|
||||
|
||||
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
|
||||
public AdditionalInstallersPage(String gameVersion, GameInstanceManifest manifest, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
this.manifest = manifest;
|
||||
this.repository = repository;
|
||||
|
||||
txtName.setText(version.getId());
|
||||
txtName.setText(manifest.id().toString());
|
||||
txtName.setEditable(false);
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
@@ -81,8 +80,8 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
Version resolved = version.resolvePreservingPatches(repository);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
|
||||
GameInstanceManifest.Resolved resolvedManifest = repository.resolve(manifest);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolvedManifest, repository.getGameVersion(manifest).orElse(null));
|
||||
String game = analyzer.getVersion(MINECRAFT).orElse(null);
|
||||
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
|
||||
|
||||
@@ -111,4 +110,13 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean showExtendPane() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetDefaultName() {
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
@@ -42,10 +43,10 @@ import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.HMCLLocalizedDownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.ui.instances.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.instances.HMCLLocalizedDownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameInstancePage;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
@@ -68,11 +69,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_MOD =
|
||||
public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_MOD =
|
||||
(downloadProvider, repository, version, mod, file) -> download(downloadProvider, repository, version, file, "mods");
|
||||
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK =
|
||||
public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_RESOURCE_PACK =
|
||||
(downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks");
|
||||
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER =
|
||||
public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_SHADER =
|
||||
(downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks");
|
||||
|
||||
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
|
||||
@@ -92,16 +93,16 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DownloadPage(String uploadVersion) {
|
||||
public DownloadPage(GameInstanceID uploadInstance) {
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> {
|
||||
Versions.downloadModpackImpl(downloadProvider, repository, uploadVersion, modpack, file);
|
||||
Instances.downloadModpackImpl(downloadProvider, repository, uploadInstance, modpack, file);
|
||||
}, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
|
||||
installLocalModpackButton.setOnAction(e -> Instances.importModpack());
|
||||
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
@@ -134,17 +135,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (node instanceof VersionPage.GameInstanceLoadable loadable) {
|
||||
if (node instanceof GameInstancePage.GameInstanceLoadable loadable) {
|
||||
loadable.loadInstance(GameDirectoryManager.getSelectedRepository(), null);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon.Version file, String subdirectoryName) {
|
||||
if (version == null) version = repository.getSelectedInstance();
|
||||
public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon.Version file, String subdirectoryName) {
|
||||
if (instanceId == null) {
|
||||
instanceId = repository.getSelectedInstance();
|
||||
}
|
||||
|
||||
Path runDirectory = repository.hasVersion(version) ? repository.getRunDirectory(version) : repository.getBaseDirectory();
|
||||
Path runDirectory = instanceId != null && repository.hasInstance(instanceId) ? repository.getRunDirectory(instanceId) : repository.getBaseDirectory();
|
||||
|
||||
Set<String> existingFiles;
|
||||
|
||||
@@ -306,10 +309,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameBuilder builder = dependencyManager.gameBuilder();
|
||||
GameBuilder builder = dependencyManager.newGameBuilder();
|
||||
|
||||
String name = (String) settings.get("name");
|
||||
builder.name(name);
|
||||
GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
builder.name(instanceId);
|
||||
builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion());
|
||||
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
@@ -318,10 +321,11 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
|
||||
repository.applyDefaultIsolationSettingForNewInstance(instanceId, settings.isInstallingModdedVersion());
|
||||
return builder.buildAsync().whenComplete(any -> {
|
||||
repository.refreshVersions();
|
||||
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
repository.refresh();
|
||||
repository.applyDefaultIsolationSetting(instanceId);
|
||||
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
@@ -42,8 +43,8 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
txtName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId));
|
||||
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
|
||||
|
||||
txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true);
|
||||
@@ -91,22 +92,22 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
}
|
||||
|
||||
protected void onInstall() {
|
||||
String name = txtName.getText();
|
||||
GameInstanceID instanceId = new GameInstanceID(txtName.getText());
|
||||
|
||||
if (!checkName(name)) {
|
||||
if (!checkName(instanceId.id())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
controller.getSettings().put("name", name);
|
||||
controller.getSettings().put(INSTANCE_ID, instanceId);
|
||||
controller.onFinish();
|
||||
}, () -> {
|
||||
// The user selects Cancel and does nothing.
|
||||
})
|
||||
.build());
|
||||
} else {
|
||||
controller.getSettings().put("name", name);
|
||||
controller.getSettings().put(INSTANCE_ID, instanceId);
|
||||
controller.onFinish();
|
||||
}
|
||||
}
|
||||
@@ -143,4 +144,15 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
txtName.setText(nameBuilder.toString());
|
||||
isNameModifiedByUser = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean showExtendPane() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetDefaultName() {
|
||||
isNameModifiedByUser = false;
|
||||
setTxtNameWithLoaders();
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,8 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
if (installAsVersion) {
|
||||
txtModpackName.getValidators().setAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId));
|
||||
} else {
|
||||
txtModpackName.getValidators().setAll(
|
||||
new RequiredValidator(),
|
||||
@@ -75,7 +75,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
&& GameDirectoryManager.getGameDirectories().stream()
|
||||
.noneMatch(existingProfile ->
|
||||
str.equals(GameDirectoryManager.getGameDirectoryCustomName(existingProfile)))),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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 = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
selectedFile = Controllers.showOpenDialog(chooser);
|
||||
if (selectedFile == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
|
||||
+13
-10
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
@@ -57,14 +58,14 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
this(repository, modpackFile, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, String updateVersion) {
|
||||
this(repository, null, updateVersion);
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, GameInstanceID updateInstanceId) {
|
||||
this(repository, null, updateInstanceId);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, String updateVersion) {
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, GameInstanceID updateInstanceId) {
|
||||
this.repository = repository;
|
||||
this.file = modpackFile;
|
||||
this.updateVersion = updateVersion;
|
||||
this.updateVersion = updateInstanceId != null ? updateInstanceId.toString() : null;
|
||||
}
|
||||
|
||||
public void setIconUrl(String iconUrl) {
|
||||
@@ -99,6 +100,8 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
|
||||
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
|
||||
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
|
||||
if (updateVersion != null) {
|
||||
if (selected == null) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -106,9 +109,9 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
try {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name)));
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name)));
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId)));
|
||||
}
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -120,11 +123,11 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
return null;
|
||||
} else {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getInstallTask(repository, serverModpackManifest, name, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
} else {
|
||||
return ModpackHelper.getInstallTask(repository, selected, name, modpack, iconUrl)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
var archiveNamePane = new LinePane();
|
||||
{
|
||||
FXUtils.setLimitHeight(archiveNamePane, 75);
|
||||
|
||||
archiveNamePane.setTitle(i18n("version.name"));
|
||||
archiveNamePane.setTitle(i18n("instance.name"));
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.setPrefWidth(300);
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -42,9 +43,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;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -112,9 +111,8 @@ 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 = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,15 +156,15 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
TaskCancellationAction.NORMAL
|
||||
);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handler.reject(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
handler.reject(i18n("message.failed"));
|
||||
}
|
||||
}, "", new URLValidator());
|
||||
}
|
||||
|
||||
public void onChooseRepository() {
|
||||
String modPackName = controller.getSettings().get(MODPACK_NAME);
|
||||
DownloadPage downloadPage = new DownloadPage(modPackName);
|
||||
DownloadPage downloadPage = new DownloadPage(modPackName != null ? new GameInstanceID(modPackName) : null);
|
||||
downloadPage.showModpackDownloads();
|
||||
Controllers.navigate(downloadPage);
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId));
|
||||
}
|
||||
|
||||
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
|
||||
|
||||
+8
-8
@@ -21,8 +21,8 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -49,15 +49,15 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final String gameVersion;
|
||||
private final Version version;
|
||||
private final GameInstanceManifest manifest;
|
||||
private final String libraryId;
|
||||
private final String oldLibraryVersion;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
|
||||
public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull GameInstanceManifest manifest, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
|
||||
this.repository = repository;
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
this.manifest = manifest;
|
||||
this.libraryId = libraryId;
|
||||
this.oldLibraryVersion = oldLibraryVersion;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
@@ -76,7 +76,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
|
||||
// We remove library but not save it,
|
||||
// so if installation failed will not break down current version.
|
||||
Task<Version> ret = Task.supplyAsync(() -> version);
|
||||
Task<GameInstanceManifest> ret = Task.supplyAsync(() -> manifest);
|
||||
var hints = new ArrayList<Task.StagesHint>();
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof RemoteVersion remoteVersion) {
|
||||
@@ -91,7 +91,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshVersionsAsync()).withStagesHints(hints);
|
||||
return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshAsync()).withStagesHints(hints);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,7 +103,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
controller.onFinish();
|
||||
} else if ("game".equals(libraryId)) {
|
||||
String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion();
|
||||
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, repository, downloadProvider));
|
||||
controller.onNext(new AdditionalInstallersPage(newGameVersion, manifest, controller, repository, downloadProvider));
|
||||
} else {
|
||||
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibraryVersion, ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
|
||||
i18n("install.change_version"), controller::onFinish, controller::onCancel);
|
||||
@@ -128,7 +128,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
|
||||
public static void alertFailureMessage(Exception exception, Runnable next) {
|
||||
if (exception instanceof LibraryDownloadException) {
|
||||
String message = i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n";
|
||||
String message = i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().name()) + "\n";
|
||||
if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.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;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -213,7 +213,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
RemoteVersion oldRemoteVersion = getItem();
|
||||
|
||||
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
if (empty) {
|
||||
@@ -239,49 +239,49 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
twoLineListItem.addTag(i18n("instance.game.release"));
|
||||
imageView.setImage(GameInstanceIconType.GRASS.getIcon());
|
||||
}
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
twoLineListItem.addTag(i18n("instance.game.april_fools"));
|
||||
imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
twoLineListItem.addTag(i18n("instance.game.snapshot"));
|
||||
imageView.setImage(GameInstanceIconType.COMMAND.getIcon());
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
twoLineListItem.addTag(i18n("instance.game.old"));
|
||||
imageView.setImage(GameInstanceIconType.CRAFT_TABLE.getIcon());
|
||||
}
|
||||
}
|
||||
|
||||
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("instance.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("instance.game.support_status.unsupported"));
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
GameInstanceIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
iconType = GameInstanceIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
iconType = GameInstanceIconType.OPTIFINE;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
iconType = GameInstanceIconType.FORGE;
|
||||
else if (remoteVersion instanceof CleanroomRemoteVersion)
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
iconType = GameInstanceIconType.CLEANROOM;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
iconType = GameInstanceIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.LEGACY_FABRIC;
|
||||
iconType = GameInstanceIconType.LEGACY_FABRIC;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
iconType = GameInstanceIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
iconType = GameInstanceIconType.QUILT;
|
||||
else
|
||||
iconType = VersionIconType.COMMAND;
|
||||
iconType = GameInstanceIconType.COMMAND;
|
||||
|
||||
imageView.setImage(iconType.getIcon());
|
||||
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
|
||||
@@ -334,7 +334,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
{
|
||||
nameField = new JFXTextField();
|
||||
nameField.setPromptText(i18n("version.search.prompt"));
|
||||
nameField.setPromptText(i18n("instance.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
@@ -354,7 +354,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
|
||||
}
|
||||
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.setConverter(stringConverter(type -> i18n("instance.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
|
||||
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
@@ -362,13 +362,13 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("version.game.type")), categoryField,
|
||||
new Label(i18n("instance.search")), nameField,
|
||||
new Label(i18n("instance.game.type")), categoryField,
|
||||
refreshButton
|
||||
);
|
||||
} else {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("instance.search")), nameField,
|
||||
refreshButton
|
||||
);
|
||||
}
|
||||
@@ -394,7 +394,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
RemoteVersionListCell dummyCell = new RemoteVersionListCell(control);
|
||||
dummyCell.twoLineListItem.setTitle("Dummy");
|
||||
dummyCell.twoLineListItem.setSubtitle("Dummy");
|
||||
dummyCell.imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
dummyCell.imageView.setImage(GameInstanceIconType.GRASS.getIcon());
|
||||
prepareNode(dummyCell.pane);
|
||||
list.setFixedCellSize(dummyCell.pane.prefHeight(-1));
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
@@ -47,11 +48,11 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
|
||||
public final class ExportWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final String version;
|
||||
private final GameInstanceID instanceId;
|
||||
|
||||
public ExportWizardProvider(HMCLGameRepository repository, String version) {
|
||||
public ExportWizardProvider(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -164,7 +165,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(repository, version, exportInfo, modpackFile);
|
||||
dependency = new McbbsModpackExportTask(repository, instanceId, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -184,8 +185,8 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
GameSettings.Effective setting = repository.getEffectiveGameSettings(version);
|
||||
dependency = new MultiMCModpackExportTask(repository, version, exportInfo.getWhitelist(),
|
||||
GameSettings.Effective setting = repository.getEffectiveGameSettings(instanceId);
|
||||
dependency = new MultiMCModpackExportTask(repository, instanceId, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
@@ -232,7 +233,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(repository, version, exportInfo, modpackFile);
|
||||
dependency = new ServerModpackExportTask(repository, instanceId, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -254,7 +255,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
repository,
|
||||
version,
|
||||
instanceId,
|
||||
exportInfo,
|
||||
modpackFile
|
||||
);
|
||||
@@ -271,8 +272,8 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
return switch (step) {
|
||||
case 0 -> new ModpackTypeSelectionPage(controller);
|
||||
case 1 -> new ModpackInfoPage(controller, repository, version);
|
||||
case 2 -> new ModpackFileSelectionPage(controller, repository, version, ModAdviser::suggestMod);
|
||||
case 1 -> new ModpackInfoPage(controller, repository, instanceId);
|
||||
case 2 -> new ModpackFileSelectionPage(controller, repository, instanceId, ModAdviser::suggestMod);
|
||||
default -> throw new IllegalArgumentException("step");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,16 +27,21 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -44,6 +49,7 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -56,37 +62,69 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class ModpackFileSelectionPage extends BorderPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final String version;
|
||||
private final GameInstanceID instanceId;
|
||||
private final ModAdviser adviser;
|
||||
private final ModpackFileTreeItem rootNode;
|
||||
private @Nullable ModpackFileTreeItem rootNode;
|
||||
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, String version, ModAdviser adviser) {
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId, ModAdviser adviser) {
|
||||
this.controller = controller;
|
||||
this.version = version;
|
||||
this.instanceId = instanceId;
|
||||
this.adviser = adviser;
|
||||
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
rootNode = getTreeItem(repository.getRunDirectory(version), "minecraft", 0);
|
||||
treeView.setRoot(rootNode);
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
onEscPressed(treeView, () -> controller.onPrev(true));
|
||||
setMargin(treeView, new Insets(10, 10, 5, 10));
|
||||
this.setCenter(treeView);
|
||||
|
||||
Label placeholder = new Label(i18n("modpack.files.empty"));
|
||||
StackPane placeholderPane = new StackPane(placeholder);
|
||||
placeholderPane.getStyleClass().add("notice-pane");
|
||||
placeholderPane.setVisible(false);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
StackPane center = new StackPane(treeView, placeholderPane);
|
||||
spinnerPane.setContent(center);
|
||||
|
||||
setMargin(spinnerPane, new Insets(10, 10, 5, 10));
|
||||
this.setCenter(spinnerPane);
|
||||
|
||||
|
||||
HBox nextPane = new HBox();
|
||||
nextPane.setPadding(new Insets(16, 16, 16, 0));
|
||||
nextPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
|
||||
btnNext.setPrefSize(100, 40);
|
||||
btnNext.setOnAction(e -> onNext());
|
||||
|
||||
nextPane.getChildren().setAll(btnNext);
|
||||
}
|
||||
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
|
||||
btnNext.setPrefSize(100, 40);
|
||||
btnNext.setOnAction(e -> onNext());
|
||||
nextPane.getChildren().setAll(btnNext);
|
||||
|
||||
loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext);
|
||||
spinnerPane.setOnFailedAction((__) -> loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext));
|
||||
|
||||
this.setBottom(nextPane);
|
||||
}
|
||||
|
||||
private void loadRoot(HMCLGameRepository repository, JFXTreeView<String> treeView, StackPane placeholderPane, SpinnerPane spinnerPane, JFXButton btnNext) {
|
||||
spinnerPane.setLoading(true);
|
||||
btnNext.setDisable(true);
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> getTreeItem(repository.getRunDirectory(instanceId), "minecraft", 0), Schedulers.io())
|
||||
.whenCompleteAsync((root, throwable) -> {
|
||||
if (throwable == null) {
|
||||
if (root != null) {
|
||||
treeView.setRoot(rootNode = root);
|
||||
} else {
|
||||
placeholderPane.setVisible(true);
|
||||
}
|
||||
spinnerPane.setFailedReason(null);
|
||||
btnNext.setDisable(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load modpack file tree", throwable);
|
||||
spinnerPane.setFailedReason(i18n("modpack.files.load_failed"));
|
||||
}
|
||||
spinnerPane.setLoading(false);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private ModpackFileTreeItem getTreeItem(Path file, String basePath, int level) {
|
||||
if (Files.notExists(file))
|
||||
return null;
|
||||
@@ -94,7 +132,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
boolean isDirectory = Files.isDirectory(file);
|
||||
|
||||
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
|
||||
if (basePath.length() > "minecraft/".length()) {
|
||||
if (level > 0) {
|
||||
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (isDirectory ? "/" : ""), isDirectory);
|
||||
|
||||
String fileName = FileUtils.getName(file);
|
||||
@@ -107,12 +145,12 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
}
|
||||
if (fileName.startsWith("._")) // macOS system file
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (FileUtils.getNameWithoutExtension(file).equals(version))
|
||||
if (FileUtils.getNameWithoutExtension(file).equals(instanceId.toString()))
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
|
||||
if (isDirectory) {
|
||||
if (fileName.equals(version + "-natives")) { // Ignore <version>-natives
|
||||
if (fileName.equals(instanceId + "-natives")) { // Ignore <version>-natives
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
if (level == 1 && fileName.startsWith("natives-")) { // Ignore natives-os-arch
|
||||
@@ -123,7 +161,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return null;
|
||||
}
|
||||
|
||||
ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? version : StringUtils.substringAfterLast(basePath, '/'), basePath);
|
||||
ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? instanceId.toString() : StringUtils.substringAfterLast(basePath, '/'), basePath);
|
||||
if (state == ModAdviser.ModSuggestion.SUGGESTED)
|
||||
node.setSelected(true);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackManifest;
|
||||
@@ -47,7 +48,6 @@ 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;
|
||||
@@ -65,9 +65,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final HMCLGameRepository gameRepository;
|
||||
private final HMCLGameRepository repository;
|
||||
private final ModpackExportInfo.Options options;
|
||||
private final String versionName;
|
||||
private final GameInstanceID instanceId;
|
||||
private final boolean canIncludeLauncher;
|
||||
|
||||
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
|
||||
@@ -88,19 +88,19 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final SimpleBooleanProperty noCreateRemoteFiles = new SimpleBooleanProperty();
|
||||
private final SimpleBooleanProperty skipCurseForgeRemoteFiles = new SimpleBooleanProperty();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.controller = controller;
|
||||
this.gameRepository = gameRepository;
|
||||
this.repository = repository;
|
||||
this.options = controller.getSettings().get(MODPACK_INFO_OPTION);
|
||||
this.versionName = version;
|
||||
this.instanceId = instanceId;
|
||||
|
||||
if (this.options == null)
|
||||
throw new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required");
|
||||
|
||||
name.set(version);
|
||||
name.set(instanceId.toString());
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
|
||||
|
||||
GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName);
|
||||
GameSettings.Effective versionSetting = repository.getEffectiveGameSettings(this.instanceId);
|
||||
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
|
||||
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
|
||||
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
|
||||
@@ -118,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 = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
Path file = Controllers.showSaveDialog(fileChooser);
|
||||
if (file == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
@@ -213,7 +213,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
var instanceNamePane = new LineTextPane();
|
||||
{
|
||||
instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
|
||||
instanceNamePane.setText(skinnable.versionName);
|
||||
instanceNamePane.setText(skinnable.instanceId.toString());
|
||||
|
||||
list.getContent().add(instanceNamePane);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ import org.jackhuang.hmcl.setting.property.SettingProperty;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionIconDialog;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameInstanceIconDialog;
|
||||
import org.jackhuang.hmcl.ui.instances.GameInstancePage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.ServerAddress;
|
||||
@@ -76,7 +76,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
implements DecoratorPage, VersionPage.GameInstanceLoadable, PageAware {
|
||||
implements DecoratorPage, GameInstancePage.GameInstanceLoadable, PageAware {
|
||||
|
||||
private static final Object INHERIT_BUTTON_TOOLTIP_KEY = new Object();
|
||||
private static final PseudoClass PSEUDO_OVERRIDDEN = PseudoClass.getPseudoClass("overridden");
|
||||
@@ -95,7 +95,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
private @Nullable HMCLGameRepository repository;
|
||||
|
||||
/// The current instance ID.
|
||||
private @Nullable String instanceId;
|
||||
private @Nullable GameInstanceID instanceId;
|
||||
|
||||
/// The current setting.
|
||||
private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting");
|
||||
@@ -787,6 +787,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
graphicsSettings.getContent().add(vulkanRendererPane);
|
||||
vulkanRendererPane.setTitle(i18n("settings.advanced.renderer.vulkan"));
|
||||
|
||||
var highPerformancePane = createInheritableBooleanButton(GameSettings::highPerformanceProperty);
|
||||
graphicsSettings.getContent().add(highPerformancePane);
|
||||
highPerformancePane.setTitle(i18n("settings.advanced.renderer.gpu_preferences"));
|
||||
highPerformancePane.setSubtitle(i18n("settings.advanced.windows_only"));
|
||||
|
||||
this.currentGameVersionNumber.addListener((o, oldValue, newValue) -> {
|
||||
boolean showBackendChoose = isPresetSetting || newValue.compareTo("26.2-snapshot-2") >= 0;
|
||||
graphicsBackendPane.setVisible(showBackendChoose);
|
||||
@@ -1721,7 +1726,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
refresh.invalidated(instance);
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
fireEvent(new GameInstancePage.WorkingDirChangedEvent());
|
||||
});
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1811,7 +1816,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
refresh.invalidated(instance);
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
fireEvent(new GameInstancePage.WorkingDirChangedEvent());
|
||||
|
||||
event.consume();
|
||||
});
|
||||
@@ -1833,7 +1838,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
updating.value = false;
|
||||
}
|
||||
|
||||
fireEvent(new VersionPage.WorkingDirChangedEvent());
|
||||
fireEvent(new GameInstancePage.WorkingDirChangedEvent());
|
||||
});
|
||||
|
||||
currentSetting.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1864,7 +1869,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
return "";
|
||||
}
|
||||
|
||||
return repository.getVersionRoot(instanceId).toString();
|
||||
return repository.getInstanceRoot(instanceId).toString();
|
||||
}
|
||||
|
||||
/// Keeps a listener attached to the current instance's parent preset property.
|
||||
@@ -2625,7 +2630,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.gameDirectory = repository.getGameDirectory();
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
@@ -2652,6 +2657,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the loaded instance ID, or `null` when this page edits preset settings.
|
||||
private @Nullable GameInstanceID getLoadedInstanceId() {
|
||||
return instanceId == null ? null : instanceId;
|
||||
}
|
||||
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
@@ -2691,12 +2701,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
/// Backs up and overwrites the current instance's `instance-game-settings.json`.
|
||||
private void forceOverwriteInstanceGameSettings() {
|
||||
if (repository == null || instanceId == null) {
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.confirmBackupAndOverwrite(i18n("settings.game.instance_settings.unsupported"), () -> {
|
||||
repository.forceOverwriteInstanceGameSettings(instanceId);
|
||||
repository.forceOverwriteInstanceGameSettings(loadedInstanceId);
|
||||
setSettingsReadOnly(false, "");
|
||||
});
|
||||
}
|
||||
@@ -2710,10 +2721,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
if (repository == null || instanceId == null)
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null)
|
||||
return;
|
||||
|
||||
iconPickerItem.setImage(repository.getVersionIconImage(instanceId));
|
||||
iconPickerItem.setImage(repository.getInstanceIconImage(loadedInstanceId));
|
||||
}
|
||||
|
||||
/// Refreshes Java selection controls and keeps inherited parent Java properties observed.
|
||||
@@ -2782,7 +2794,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
initializeSelectedJava();
|
||||
|
||||
JavaVersionType javaVersionType = setting.javaTypeProperty().getValue();
|
||||
GameSettings.Effective effectiveSetting = this.instanceId != null && repository != null ? repository.getEffectiveGameSettings(this.instanceId) : null;
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
GameSettings.Effective effectiveSetting = loadedInstanceId != null && repository != null ? repository.getEffectiveGameSettings(loadedInstanceId) : null;
|
||||
JavaVersionType effectiveJavaVersionType = effectiveSetting != null ? effectiveSetting.getInheritable(GameSettings::javaTypeProperty) : javaVersionType;
|
||||
boolean autoSelected = effectiveJavaVersionType == JavaVersionType.AUTO || effectiveJavaVersionType == JavaVersionType.VERSION;
|
||||
|
||||
@@ -2799,17 +2812,17 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
if (JavaManager.isInitialized()) {
|
||||
GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get();
|
||||
Version version;
|
||||
GameInstanceManifest manifest;
|
||||
if (this.instanceId == null) {
|
||||
version = null;
|
||||
manifest = null;
|
||||
} else {
|
||||
version = repository != null ? repository.getResolvedVersion(this.instanceId) : null;
|
||||
manifest = repository != null && loadedInstanceId != null ? repository.getResolvedInstanceManifest(loadedInstanceId).launchManifest() : null;
|
||||
}
|
||||
|
||||
try {
|
||||
JavaRuntime java = effectiveSetting != null
|
||||
? effectiveSetting.getJava(gameVersionNumber, version)
|
||||
: resolveEffectiveSetting(setting).getJava(gameVersionNumber, version);
|
||||
? effectiveSetting.getJava(gameVersionNumber, manifest)
|
||||
: resolveEffectiveSetting(setting).getJava(gameVersionNumber, manifest);
|
||||
if (java != null) {
|
||||
javaSublist.setDescription(java.getBinary().toString());
|
||||
} else {
|
||||
@@ -2827,17 +2840,18 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
if (repository == null || instanceId == null)
|
||||
return;
|
||||
|
||||
Controllers.dialog(new VersionIconDialog(repository, instanceId, this::loadIcon));
|
||||
Controllers.dialog(new GameInstanceIconDialog(repository, instanceId, this::loadIcon));
|
||||
}
|
||||
|
||||
private void onDeleteIcon() {
|
||||
if (repository == null || instanceId == null)
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null)
|
||||
return;
|
||||
|
||||
repository.deleteIconFile(instanceId);
|
||||
GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
repository.deleteIconFile(loadedInstanceId);
|
||||
GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(loadedInstanceId);
|
||||
if (localGameSettings != null) {
|
||||
localGameSettings.iconProperty().setValue(VersionIconType.DEFAULT);
|
||||
localGameSettings.iconProperty().setValue(GameInstanceIconType.DEFAULT);
|
||||
}
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.ui.construct.LineInheritableToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.RadioChoiceList;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -621,7 +622,7 @@ final class IndependentSettingBinder {
|
||||
|
||||
long memoryAllocated;
|
||||
if (Boolean.TRUE.equals(autoMemory)) {
|
||||
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available());
|
||||
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available(), Platform.SYSTEM_PLATFORM);
|
||||
} else if (maxMemory != null && maxMemory > 0) {
|
||||
memoryAllocated = maxMemory * 1024L * 1024L;
|
||||
} else {
|
||||
|
||||
@@ -51,7 +51,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -85,37 +84,38 @@ public final class ImageUtils {
|
||||
LoaderParameters parameters = new LoaderParameters();
|
||||
parameters.autoStartAnimations = false;
|
||||
|
||||
Supplier<Image> supplier = () -> {
|
||||
SVGImage image = SVGLoader.load(content, parameters);
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable Image image;
|
||||
@Nullable SVGImage image;
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
image = supplier.get();
|
||||
image = SVGLoader.load(content, parameters);
|
||||
} else {
|
||||
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
|
||||
image = CompletableFuture.supplyAsync(supplier, Schedulers.javafx()).get();
|
||||
image = CompletableFuture.supplyAsync(
|
||||
() -> SVGLoader.load(content, parameters),
|
||||
Schedulers.javafx()
|
||||
).get();
|
||||
}
|
||||
|
||||
return image;
|
||||
if (image == null)
|
||||
throw new IOException("Failed to load SVG image");
|
||||
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
+112
-11
@@ -15,31 +15,33 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTableCell;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
@@ -83,26 +85,42 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
enabledColumn.setMinWidth(40);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> fileNameColumn = new TableColumn<>(i18n("addon.check_update.file"));
|
||||
fileNameColumn.setPrefWidth(200);
|
||||
fileNameColumn.setPrefWidth(180);
|
||||
setupCellValueFactory(fileNameColumn, AddonUpdateObject::fileNameProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> currentVersionColumn = new TableColumn<>(i18n("addon.check_update.current_version"));
|
||||
currentVersionColumn.setPrefWidth(200);
|
||||
currentVersionColumn.setPrefWidth(180);
|
||||
setupCellValueFactory(currentVersionColumn, AddonUpdateObject::currentVersionProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> targetVersionColumn = new TableColumn<>(i18n("addon.check_update.target_version"));
|
||||
targetVersionColumn.setPrefWidth(200);
|
||||
targetVersionColumn.setPrefWidth(180);
|
||||
setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("addon.check_update.source"));
|
||||
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> changelogColumn = new TableColumn<>(i18n("addon.changelog"));
|
||||
{
|
||||
var oldCellFactory = changelogColumn.getCellFactory();
|
||||
changelogColumn.setCellFactory(param -> {
|
||||
TableCell<AddonUpdateObject, String> cell = oldCellFactory.call(param);
|
||||
cell.getStyleClass().add("addon-changelog-table-cell");
|
||||
cell.setOnMouseClicked(event -> {
|
||||
AddonUpdateObject object;
|
||||
if (cell.isEmpty() || cell.getTableRow() == null || (object = cell.getTableRow().getItem()) == null) return;
|
||||
Controllers.dialog(new AddonChangelog(object));
|
||||
});
|
||||
return cell;
|
||||
});
|
||||
changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("button.view")));
|
||||
}
|
||||
|
||||
objects = FXCollections.observableList(updates.stream().map(AddonUpdateObject::new).collect(Collectors.toList()));
|
||||
FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new));
|
||||
|
||||
TableView<AddonUpdateObject> table = new TableView<>(objects);
|
||||
table.setEditable(true);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, changelogColumn);
|
||||
setMargin(table, new Insets(10, 10, 5, 10));
|
||||
|
||||
setCenter(table);
|
||||
@@ -198,6 +216,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
final StringProperty currentVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersion = new SimpleStringProperty();
|
||||
final StringProperty source = new SimpleStringProperty();
|
||||
String changelog = null;
|
||||
|
||||
public AddonUpdateObject(LocalAddonFile.AddonUpdate data) {
|
||||
this.data = data;
|
||||
@@ -206,7 +225,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
fileName.set(data.localAddonFile().getFileName());
|
||||
currentVersion.set(data.currentVersion().version());
|
||||
targetVersion.set(data.targetVersion().version());
|
||||
switch (data.currentVersion().self().getType()) {
|
||||
switch (data.currentVersion().self().getSource()) {
|
||||
case CURSEFORGE:
|
||||
source.set(i18n("addon.curseforge"));
|
||||
break;
|
||||
@@ -280,6 +299,88 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AddonChangelog extends JFXDialogLayout {
|
||||
|
||||
public AddonChangelog(AddonUpdateObject object) {
|
||||
RemoteAddon.Version targetVersion = object.data.targetVersion();
|
||||
|
||||
this.setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + targetVersion.name())));
|
||||
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8));
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.setOverflowHidden(scrollPane, 8);
|
||||
|
||||
loadChangelog(object, spinnerPane, scrollPane);
|
||||
spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane));
|
||||
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
|
||||
this.setBody(box);
|
||||
|
||||
JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url"));
|
||||
versionPageBtn.setDisable(true);
|
||||
loadVersionPageUrl(object, versionPageBtn);
|
||||
|
||||
JFXButton closeButton = new JFXButton(i18n("button.ok"));
|
||||
closeButton.getStyleClass().add("dialog-accept");
|
||||
closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
setActions(versionPageBtn, closeButton);
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.7));
|
||||
|
||||
onEscPressed(this, closeButton::fire);
|
||||
}
|
||||
|
||||
private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) {
|
||||
spinnerPane.setLoading(true);
|
||||
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
|
||||
Task.supplyAsync(() -> {
|
||||
if (object.changelog != null) {
|
||||
return object.changelog;
|
||||
}
|
||||
RemoteAddon.Version version = object.data.targetVersion();
|
||||
if (repo == null) return null;
|
||||
return StringUtils.convertToHtml(
|
||||
repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.projectId(), version.versionId()),
|
||||
"238222".equals(object.data.targetVersion().projectId())
|
||||
);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl()));
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
spinnerPane.setFailedReason(i18n("download.failed.refresh"));
|
||||
}
|
||||
spinnerPane.setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) {
|
||||
Task.supplyAsync(() -> {
|
||||
RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType());
|
||||
return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null && StringUtils.isNotBlank(result)) {
|
||||
button.setExternalLink(result);
|
||||
button.setDisable(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load addon version page url", exception);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddonUpdateTask extends Task<Void> {
|
||||
private final Collection<Task<?>> dependents;
|
||||
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
|
||||
+2
-3
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -91,8 +91,7 @@ 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 = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
if (res != null) {
|
||||
installMultiDataPack(res);
|
||||
}
|
||||
+38
-6
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
@@ -25,6 +25,7 @@ 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;
|
||||
@@ -33,6 +34,7 @@ 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;
|
||||
@@ -51,7 +53,9 @@ 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;
|
||||
@@ -68,6 +72,7 @@ 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;
|
||||
@@ -79,10 +84,15 @@ 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;
|
||||
|
||||
@@ -148,16 +158,21 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
|
||||
searchPause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
});
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
isSearching.set(false);
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
filteredList.setPredicate(null);
|
||||
|
||||
isSearching.set(false);
|
||||
});
|
||||
FXUtils.onEscPressed(searchField, closeSearchBar::fire);
|
||||
searchBar.getChildren().addAll(searchField, closeSearchBar);
|
||||
@@ -193,6 +208,23 @@ 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);
|
||||
|
||||
+42
-35
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
@@ -38,8 +38,9 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
@@ -67,17 +68,17 @@ import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.GameInstanceLoadable {
|
||||
public class DownloadListPage extends Control implements DecoratorPage, GameInstancePage.GameInstanceLoadable {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty failed = new SimpleBooleanProperty(false);
|
||||
private final boolean versionSelection;
|
||||
private final boolean instanceSelection;
|
||||
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty pageOffset = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty pageCount = new SimpleIntegerProperty(-1);
|
||||
private final ListProperty<RemoteAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final ObservableList<String> versions = FXCollections.observableArrayList();
|
||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||
private final ObservableList<GameInstanceID> instances = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<GameInstanceID> selectedInstance = new SimpleObjectProperty<>();
|
||||
private final DownloadPage.DownloadCallback callback;
|
||||
private boolean searchInitialized = false;
|
||||
protected final BooleanProperty supportChinese = new SimpleBooleanProperty();
|
||||
@@ -95,10 +96,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this(repository, null, false);
|
||||
}
|
||||
|
||||
public DownloadListPage(RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
public DownloadListPage(RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
this.repository = repository;
|
||||
this.callback = callback;
|
||||
this.versionSelection = versionSelection;
|
||||
this.instanceSelection = instanceSelection;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId));
|
||||
|
||||
setLoading(false);
|
||||
@@ -122,11 +123,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
search("", null, 0, "", RemoteAddonRepository.SortType.POPULARITY);
|
||||
}
|
||||
|
||||
if (versionSelection) {
|
||||
versions.setAll(repository.getDisplayVersions()
|
||||
.map(Version::getId)
|
||||
.collect(Collectors.toList()));
|
||||
selectedVersion.set(repository.getSelectedInstance());
|
||||
if (instanceSelection) {
|
||||
instances.setAll(repository.getDisplayInstanceManifests()
|
||||
.map(GameInstanceManifest::id)
|
||||
.toList());
|
||||
selectedInstance.set(repository.getSelectedInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +155,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
public void selectVersion(String versionID) {
|
||||
FXUtils.runInFX(() -> selectedVersion.set(versionID));
|
||||
public void selectInstance(GameInstanceID instanceId) {
|
||||
FXUtils.runInFX(() -> selectedInstance.set(instanceId));
|
||||
}
|
||||
|
||||
private void search(String userGameVersion, RemoteAddonRepository.Category category, int pageOffset, String searchFilter, RemoteAddonRepository.SortType sort) {
|
||||
@@ -166,12 +167,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
int currentSearchID = searchID = searchID + 1;
|
||||
Task.supplyAsync(() -> {
|
||||
HMCLGameRepository.InstanceReference instanceReference = this.instanceReference.get();
|
||||
if (StringUtils.isBlank(instanceReference.instanceId())) {
|
||||
if (instanceReference.instanceId() == null) {
|
||||
return userGameVersion;
|
||||
} else {
|
||||
return StringUtils.isNotBlank(instanceReference.instanceId())
|
||||
? instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse("")
|
||||
: "";
|
||||
return instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse("");
|
||||
}
|
||||
}).thenApplyAsync(
|
||||
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC)
|
||||
@@ -219,8 +218,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
}
|
||||
|
||||
protected HMCLGameRepository.InstanceReference getInstanceReference() {
|
||||
if (versionSelection) {
|
||||
return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), selectedVersion.get());
|
||||
if (instanceSelection) {
|
||||
@Nullable GameInstanceID instanceId = selectedInstance.get();
|
||||
return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), instanceId);
|
||||
} else {
|
||||
return instanceReference.get();
|
||||
}
|
||||
@@ -273,18 +273,18 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
{
|
||||
int rowIndex = 0;
|
||||
|
||||
if (control.versionSelection || !control.downloadSources.isEmpty()) {
|
||||
if (control.instanceSelection || !control.downloadSources.isEmpty()) {
|
||||
searchPane.addRow(rowIndex);
|
||||
int columns = 0;
|
||||
Node lastNode = null;
|
||||
if (control.versionSelection) {
|
||||
JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
|
||||
versionsComboBox.setMaxWidth(Double.MAX_VALUE);
|
||||
Bindings.bindContent(versionsComboBox.getItems(), control.versions);
|
||||
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion);
|
||||
if (control.instanceSelection) {
|
||||
JFXComboBox<GameInstanceID> instancesComboBox = new JFXComboBox<>();
|
||||
instancesComboBox.setMaxWidth(Double.MAX_VALUE);
|
||||
Bindings.bindContent(instancesComboBox.getItems(), control.instances);
|
||||
selectedItemPropertyFor(instancesComboBox).bindBidirectional(control.selectedInstance);
|
||||
|
||||
searchPane.add(new Label(i18n("version")), columns++, rowIndex);
|
||||
searchPane.add(lastNode = versionsComboBox, columns++, rowIndex);
|
||||
searchPane.add(new Label(i18n("instance")), columns++, rowIndex);
|
||||
searchPane.add(lastNode = instancesComboBox, columns++, rowIndex);
|
||||
}
|
||||
|
||||
if (control.downloadSources.getSize() > 1) {
|
||||
@@ -329,7 +329,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
FXUtils.installFastTooltip(gameVersionField, i18n("search.enter"));
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().instanceReference, instanceReference -> {
|
||||
if (StringUtils.isNotBlank(instanceReference.instanceId())) {
|
||||
if (instanceReference.instanceId() != null) {
|
||||
GridPane.setColumnSpan(nameField, 3);
|
||||
} else {
|
||||
GridPane.setColumnSpan(nameField, 1);
|
||||
@@ -519,13 +519,13 @@ 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("download.failed.no_results_found");
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getSkinnable().failedProperty(),
|
||||
getSkinnable().loadingProperty(),
|
||||
getSkinnable().failedProperty(),
|
||||
getSkinnable().loadingProperty(),
|
||||
getSkinnable().pageCount,
|
||||
getSkinnable().items)
|
||||
);
|
||||
@@ -542,6 +542,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
private static final Insets PADDING = new Insets(9, 9, 0, 9);
|
||||
private static final Insets LAST_PADDING = new Insets(9, 9, 9, 9);
|
||||
|
||||
private final RipplerContainer graphic;
|
||||
private final StackPane wrapper = new StackPane();
|
||||
@@ -584,7 +585,13 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.slug());
|
||||
setPadding(
|
||||
getIndex() == getListView().getItems().size() - 1
|
||||
? LAST_PADDING
|
||||
: PADDING
|
||||
);
|
||||
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(getSkinnable().repository.getType()).getModByCurseForgeId(item.slug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.title());
|
||||
String description = item.description();
|
||||
if (description != null) {
|
||||
+144
-44
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
@@ -31,8 +31,9 @@ import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
@@ -42,12 +43,11 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
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;
|
||||
|
||||
@@ -57,8 +57,10 @@ import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
private final BooleanProperty loaded = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(false);
|
||||
@@ -70,7 +72,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
private final HMCLGameRepository.InstanceReference instanceReference;
|
||||
private final DownloadCallback callback;
|
||||
private final DownloadListPage page;
|
||||
private final RemoteAddonRepository.Type type;
|
||||
private final RemoteAddon.Type type;
|
||||
|
||||
private SimpleMultimap<String, RemoteAddon.Version, List<RemoteAddon.Version>> versions;
|
||||
|
||||
@@ -78,8 +80,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.page = page;
|
||||
this.repository = page.repository;
|
||||
this.addon = addon;
|
||||
this.type = Objects.requireNonNullElse(addon.repoType(), repository.getType());
|
||||
this.translations = ModTranslations.getTranslationsByRepositoryType(this.type);
|
||||
this.type = Objects.requireNonNullElse(addon.type(), repository.getType());
|
||||
this.translations = ModTranslations.getTranslationsByAddonType(this.type);
|
||||
this.mod = translations.getModByCurseForgeId(addon.slug());
|
||||
this.instanceReference = instanceReference;
|
||||
this.callback = callback;
|
||||
@@ -171,7 +173,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 = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
Path dest = Controllers.showSaveDialog(fileChooser);
|
||||
if (dest == null) {
|
||||
return;
|
||||
}
|
||||
@@ -271,30 +273,30 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
if (control.instanceReference.repository() != null && control.instanceReference.instanceId() != null) {
|
||||
HMCLGameRepository repository = control.instanceReference.repository();
|
||||
Version game = repository.getResolvedPreservingPatchesVersion(control.instanceReference.instanceId());
|
||||
String gameVersion = repository.getGameVersion(game).orElse(null);
|
||||
GameInstanceManifest.Resolved resolvedManifest = repository.getResolvedInstanceManifest(control.instanceReference.instanceId());
|
||||
String gameVersion = repository.getGameVersion(resolvedManifest.unresolved()).orElse(null);
|
||||
|
||||
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
|
||||
List<RemoteAddon.Version> modVersions = control.versions.get(gameVersion);
|
||||
if (modVersions != null && !modVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
|
||||
List<RemoteAddon.Version> addonVersions = control.versions.get(gameVersion);
|
||||
if (addonVersions != null && !addonVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(resolvedManifest, gameVersion).getModLoaders();
|
||||
|
||||
resolve:
|
||||
for (RemoteAddon.Version modVersion : modVersions) {
|
||||
if (getSkinnable().type == RemoteAddonRepository.Type.MOD) {
|
||||
for (ModLoaderType loader : modVersion.loaders()) {
|
||||
for (RemoteAddon.Version addonVersion : addonVersions) {
|
||||
if (getSkinnable().type == RemoteAddon.Type.MOD) {
|
||||
for (ModLoaderType loader : addonVersion.loaders()) {
|
||||
if (targetLoaders.contains(loader)) {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, modVersion, control)
|
||||
ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, addonVersion, control)
|
||||
);
|
||||
break resolve;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, modVersion, control)
|
||||
ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, addonVersion, control)
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -386,8 +388,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
pane.getChildren().setAll(imageView, content);
|
||||
FXUtils.setLimitHeight(this, 60);
|
||||
|
||||
RemoteAddonRepository.Type type = addon.repoType();
|
||||
DownloadCallback callback = switch (type) {
|
||||
RemoteAddon.Type type = addon.type();
|
||||
DownloadCallback callback = type == null ? null : switch (type) {
|
||||
case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD;
|
||||
case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK;
|
||||
case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER;
|
||||
@@ -400,7 +402,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
setNode(IDX_LEADING, pane);
|
||||
|
||||
if (addon != RemoteAddon.BROKEN) {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(type).getModByCurseForgeId(addon.slug());
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title());
|
||||
content.setSubtitle(addon.description());
|
||||
for (String category : addon.categories()) {
|
||||
@@ -437,19 +439,34 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
content.setTitle(dataItem.name());
|
||||
content.setSubtitle(I18n.formatDateTime(dataItem.datePublished()));
|
||||
|
||||
switch (dataItem.versionType()) {
|
||||
case Alpha:
|
||||
content.addTag(i18n("addon.channel.alpha"));
|
||||
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(24));
|
||||
break;
|
||||
case Beta:
|
||||
content.addTag(i18n("addon.channel.beta"));
|
||||
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(24));
|
||||
break;
|
||||
case Release:
|
||||
content.addTag(i18n("addon.channel.release"));
|
||||
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(24));
|
||||
break;
|
||||
@Nullable SVG svg = null;
|
||||
@Nullable String styleClass = null;
|
||||
if (dataItem.versionType() != null) {
|
||||
switch (dataItem.versionType()) {
|
||||
case Alpha:
|
||||
content.addTag(i18n("addon.channel.alpha"));
|
||||
svg = SVG.ALPHA_CIRCLE;
|
||||
styleClass = "alpha";
|
||||
break;
|
||||
case Beta:
|
||||
content.addTag(i18n("addon.channel.beta"));
|
||||
svg = SVG.BETA_CIRCLE;
|
||||
styleClass = "beta";
|
||||
break;
|
||||
case Release:
|
||||
content.addTag(i18n("addon.channel.release"));
|
||||
svg = SVG.RELEASE_CIRCLE;
|
||||
styleClass = "release";
|
||||
break;
|
||||
default:
|
||||
styleClass = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (svg != null) {
|
||||
SVGContainer icon = svg.createIcon(24);
|
||||
icon.getStyleClass().addAll("addon-channel-icon", styleClass);
|
||||
graphicPane.getChildren().setAll(icon);
|
||||
}
|
||||
|
||||
for (ModLoaderType modLoaderType : dataItem.loaders()) {
|
||||
@@ -472,6 +489,9 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +512,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
private static final class AddonVersion extends JFXDialogLayout {
|
||||
public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage selfPage) {
|
||||
RemoteAddonRepository.Type type = selfPage.type;
|
||||
RemoteAddon.Type type = selfPage.type;
|
||||
|
||||
String title = switch (type) {
|
||||
case WORLD -> "world.download.title";
|
||||
@@ -505,9 +525,20 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8));
|
||||
AddonItem addonItem = new AddonItem(mod, version, selfPage);
|
||||
var addonItem = new AddonItem(mod, version, selfPage);
|
||||
addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again
|
||||
box.getChildren().setAll(addonItem);
|
||||
|
||||
JFXHyperlink changelogButton = new JFXHyperlink(i18n("addon.changelog"));
|
||||
changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository, selfPage.page.getDownloadProvider())));
|
||||
|
||||
JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url"));
|
||||
versionPageBtn.setDisable(true);
|
||||
loadVersionPageUrl(version, selfPage.repository, versionPageBtn);
|
||||
|
||||
HBox additionalBox = new HBox(changelogButton, versionPageBtn);
|
||||
box.getChildren().add(additionalBox);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList dependenciesList = new ComponentList();
|
||||
@@ -528,10 +559,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
JFXButton downloadButton = null;
|
||||
if (selfPage.callback != null) {
|
||||
downloadButton = new JFXButton(type == RemoteAddonRepository.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install"));
|
||||
downloadButton = new JFXButton(type == RemoteAddon.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install"));
|
||||
downloadButton.getStyleClass().add("dialog-accept");
|
||||
downloadButton.setOnAction(e -> {
|
||||
if (type == RemoteAddonRepository.Type.MODPACK || !spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
if (type == RemoteAddon.Type.MODPACK || !spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
selfPage.download(mod, version);
|
||||
@@ -557,8 +588,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.setActions(downloadButton, saveAsButton, cancelButton);
|
||||
}
|
||||
|
||||
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));
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8));
|
||||
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
@@ -601,7 +632,6 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
).toList()
|
||||
);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
spinnerPane.setLoading(false);
|
||||
if (exception == null) {
|
||||
dependenciesList.getContent().setAll(result);
|
||||
spinnerPane.setFailedReason(null);
|
||||
@@ -609,12 +639,82 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
dependenciesList.getContent().setAll();
|
||||
spinnerPane.setFailedReason(i18n("download.failed.refresh"));
|
||||
}
|
||||
spinnerPane.setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void loadVersionPageUrl(RemoteAddon.Version version, RemoteAddonRepository repo, JFXHyperlink button) {
|
||||
Task.supplyAsync(() -> repo.getVersionPageUrl(version))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null && StringUtils.isNotBlank(result)) {
|
||||
button.setExternalLink(result);
|
||||
button.setDisable(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load addon version page url", exception);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AddonChangelog extends JFXDialogLayout {
|
||||
|
||||
public AddonChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider) {
|
||||
setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + version.name())));
|
||||
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8));
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.setOverflowHidden(scrollPane, 8);
|
||||
|
||||
loadChangelog(version, repo, provider, spinnerPane, scrollPane);
|
||||
spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, provider, spinnerPane, scrollPane));
|
||||
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
|
||||
this.setBody(box);
|
||||
|
||||
JFXButton closeButton = new JFXButton(i18n("button.ok"));
|
||||
closeButton.getStyleClass().add("dialog-accept");
|
||||
closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
setActions(closeButton);
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8));
|
||||
|
||||
onEscPressed(this, closeButton::fire);
|
||||
}
|
||||
|
||||
private void loadChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) {
|
||||
spinnerPane.setLoading(true);
|
||||
Task.supplyAsync(() ->
|
||||
StringUtils.convertToHtml(
|
||||
repo.getAddonChangelog(provider, version.projectId(), version.versionId()),
|
||||
"238222".equals(version.projectId()) // https://github.com/HMCL-dev/HMCL/pull/4828#issuecomment-3791068569 FUCK YOU JEI, FUCK YOU CURSEFORGE
|
||||
)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
String changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl()));
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
spinnerPane.setFailedReason(i18n("download.failed.refresh"));
|
||||
}
|
||||
spinnerPane.setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DownloadCallback {
|
||||
void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon addon, RemoteAddon.Version file);
|
||||
void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon addon, RemoteAddon.Version file);
|
||||
}
|
||||
}
|
||||
+30
-28
@@ -15,15 +15,16 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
@@ -37,11 +38,11 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
private final ImageContainer imageContainer;
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
private HMCLGameRepository repository;
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private Consumer<Event> onVersionIconChangedListener;
|
||||
@SuppressWarnings("unused")
|
||||
private Consumer<Event> onInstanceIconChangedListener;
|
||||
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private Consumer<RefreshedVersionsEvent> onRefreshedVersionsListener;
|
||||
private Consumer<RefreshedGameInstancesEvent> onRefreshedInstancesListener;
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
|
||||
@@ -49,33 +50,34 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
|
||||
setLeftGraphic(imageContainer);
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), it -> this.loadVersion()));
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadInstance));
|
||||
}
|
||||
|
||||
private void loadVersion() {
|
||||
String version = GameDirectoryManager.getSelectedInstance();
|
||||
|
||||
boolean repositoryChanged = GameDirectoryManager.getSelectedRepository() != repository;
|
||||
if (repositoryChanged) {
|
||||
private void loadInstance(GameInstanceID instanceId) {
|
||||
if (GameDirectoryManager.getSelectedRepository() != repository) {
|
||||
repository = GameDirectoryManager.getSelectedRepository();
|
||||
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
|
||||
FXUtils.runInFX(this::loadVersion);
|
||||
});
|
||||
|
||||
if (!repository.isLoaded()) {
|
||||
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
|
||||
.registerWeak(event -> FXUtils.runInFX(this::loadVersion));
|
||||
if (repository != null) {
|
||||
onInstanceIconChangedListener = repository.onInstanceIconChanged.registerWeak(event -> {
|
||||
FXUtils.runInFX(() -> loadInstance(repository.getSelectedInstance()));
|
||||
});
|
||||
if (!repository.isLoaded()) {
|
||||
onRefreshedInstancesListener = EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class)
|
||||
.registerWeak(event -> FXUtils.runInFX(() -> loadInstance(repository.getSelectedInstance())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instanceId != null && repository != null) {
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
setTitle(i18n("instance.manage.manage"));
|
||||
setSubtitle(instanceId.toString());
|
||||
imageContainer.setImage(repository.getInstanceIconImage(instanceId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (version != null && repository != null && repository.hasVersion(version)) {
|
||||
setTitle(i18n("version.manage.manage"));
|
||||
setSubtitle(version);
|
||||
imageContainer.setImage(repository.getVersionIconImage(version));
|
||||
} else {
|
||||
setTitle(i18n("version.empty"));
|
||||
setSubtitle(i18n("version.empty.add"));
|
||||
imageContainer.setImage(VersionIconType.DEFAULT.getIcon());
|
||||
}
|
||||
|
||||
setTitle(i18n("instance.empty"));
|
||||
setSubtitle(i18n("instance.empty.add"));
|
||||
imageContainer.setImage(GameInstanceIconType.DEFAULT.getIcon());
|
||||
}
|
||||
}
|
||||
+27
-27
@@ -15,22 +15,22 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
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;
|
||||
@@ -38,17 +38,17 @@ import java.nio.file.Path;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class VersionIconDialog extends DialogPane {
|
||||
public class GameInstanceIconDialog extends DialogPane {
|
||||
private final HMCLGameRepository repository;
|
||||
private final String versionId;
|
||||
private final GameInstanceID instanceId;
|
||||
private final Runnable onFinish;
|
||||
private final GameSettings.Instance setting;
|
||||
|
||||
public VersionIconDialog(HMCLGameRepository repository, String versionId, Runnable onFinish) {
|
||||
public GameInstanceIconDialog(HMCLGameRepository repository, GameInstanceID instanceId, Runnable onFinish) {
|
||||
this.repository = repository;
|
||||
this.versionId = versionId;
|
||||
this.instanceId = instanceId;
|
||||
this.onFinish = onFinish;
|
||||
this.setting = repository.getInstanceGameSettingsOrCreate(versionId);
|
||||
this.setting = repository.getInstanceGameSettingsOrCreate(this.instanceId);
|
||||
|
||||
setTitle(i18n("settings.icon"));
|
||||
FlowPane pane = new FlowPane();
|
||||
@@ -56,33 +56,33 @@ public class VersionIconDialog extends DialogPane {
|
||||
|
||||
pane.getChildren().setAll(
|
||||
createCustomIcon(),
|
||||
createIcon(VersionIconType.GRASS),
|
||||
createIcon(VersionIconType.CHEST),
|
||||
createIcon(VersionIconType.CHICKEN),
|
||||
createIcon(VersionIconType.COMMAND),
|
||||
createIcon(VersionIconType.APRIL_FOOLS),
|
||||
createIcon(VersionIconType.OPTIFINE),
|
||||
createIcon(VersionIconType.CRAFT_TABLE),
|
||||
createIcon(VersionIconType.FABRIC),
|
||||
createIcon(VersionIconType.LEGACY_FABRIC),
|
||||
createIcon(VersionIconType.FORGE),
|
||||
createIcon(VersionIconType.CLEANROOM),
|
||||
createIcon(VersionIconType.NEO_FORGE),
|
||||
createIcon(VersionIconType.FURNACE),
|
||||
createIcon(VersionIconType.QUILT)
|
||||
createIcon(GameInstanceIconType.GRASS),
|
||||
createIcon(GameInstanceIconType.CHEST),
|
||||
createIcon(GameInstanceIconType.CHICKEN),
|
||||
createIcon(GameInstanceIconType.COMMAND),
|
||||
createIcon(GameInstanceIconType.APRIL_FOOLS),
|
||||
createIcon(GameInstanceIconType.OPTIFINE),
|
||||
createIcon(GameInstanceIconType.CRAFT_TABLE),
|
||||
createIcon(GameInstanceIconType.FABRIC),
|
||||
createIcon(GameInstanceIconType.LEGACY_FABRIC),
|
||||
createIcon(GameInstanceIconType.FORGE),
|
||||
createIcon(GameInstanceIconType.CLEANROOM),
|
||||
createIcon(GameInstanceIconType.NEO_FORGE),
|
||||
createIcon(GameInstanceIconType.FURNACE),
|
||||
createIcon(GameInstanceIconType.QUILT)
|
||||
);
|
||||
}
|
||||
|
||||
private void exploreIcon() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
if (selectedFile != null) {
|
||||
try {
|
||||
repository.setVersionIconFile(versionId, selectedFile);
|
||||
repository.setInstanceIconFile(instanceId, selectedFile);
|
||||
|
||||
if (setting != null) {
|
||||
setting.iconProperty().setValue(VersionIconType.DEFAULT);
|
||||
setting.iconProperty().setValue(GameInstanceIconType.DEFAULT);
|
||||
}
|
||||
|
||||
onAccept();
|
||||
@@ -102,7 +102,7 @@ public class VersionIconDialog extends DialogPane {
|
||||
return container;
|
||||
}
|
||||
|
||||
private Node createIcon(VersionIconType type) {
|
||||
private Node createIcon(GameInstanceIconType type) {
|
||||
ImageView imageView = new ImageView(type.getIcon());
|
||||
imageView.setMouseTransparent(true);
|
||||
RipplerContainer container = new RipplerContainer(imageView);
|
||||
@@ -119,7 +119,7 @@ public class VersionIconDialog extends DialogPane {
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
repository.onVersionIconChanged.fireEvent(new Event(this));
|
||||
repository.onInstanceIconChanged.fireEvent(new Event(this));
|
||||
onFinish.run();
|
||||
super.onAccept();
|
||||
}
|
||||
+80
-75
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
@@ -28,7 +28,8 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -53,21 +54,21 @@ import java.util.function.Supplier;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public class GameInstancePage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
private final TabHeader tab;
|
||||
private final TabHeader.Tab<GameSettingsPage<GameSettings.Instance>> versionSettingsTab = new TabHeader.Tab<>("versionSettingsTab");
|
||||
private final TabHeader.Tab<GameSettingsPage<GameSettings.Instance>> gameSettingsTab = new TabHeader.Tab<>("versionSettingsTab");
|
||||
private final TabHeader.Tab<InstallerListPage> installerListTab = new TabHeader.Tab<>("installerListTab");
|
||||
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
|
||||
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
|
||||
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
|
||||
private final TabHeader.Tab<ResourcePackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
|
||||
private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private String preferredVersionName = null;
|
||||
private GameInstanceID preferredInstanceId = null;
|
||||
|
||||
public static class WorkingDirChangedEvent extends Event {
|
||||
public static final EventType<WorkingDirChangedEvent> EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED");
|
||||
@@ -77,44 +78,45 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}
|
||||
|
||||
public VersionPage() {
|
||||
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new GameSettingsPage<>(GameSettings.Instance.class)));
|
||||
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
|
||||
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcePackListPage::new));
|
||||
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
|
||||
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
|
||||
public GameInstancePage() {
|
||||
gameSettingsTab.setNodeSupplier(loadInstanceFor(() -> new GameSettingsPage<>(GameSettings.Instance.class)));
|
||||
installerListTab.setNodeSupplier(loadInstanceFor(InstallerListPage::new));
|
||||
modListTab.setNodeSupplier(loadInstanceFor(ModListPage::new));
|
||||
resourcePackTab.setNodeSupplier(loadInstanceFor(ResourcePackListPage::new));
|
||||
worldListTab.setNodeSupplier(loadInstanceFor(WorldListPage::new));
|
||||
schematicsTab.setNodeSupplier(loadInstanceFor(SchematicsPage::new));
|
||||
|
||||
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
|
||||
tab.select(versionSettingsTab);
|
||||
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
|
||||
tab.select(gameSettingsTab);
|
||||
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
|
||||
addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> {
|
||||
if (this.instanceReference.get() != null) {
|
||||
if (installerListTab.isInitialized())
|
||||
installerListTab.getNode().loadInstance(getRepository(), getVersion());
|
||||
installerListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (modListTab.isInitialized())
|
||||
modListTab.getNode().loadInstance(getRepository(), getVersion());
|
||||
modListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (resourcePackTab.isInitialized())
|
||||
resourcePackTab.getNode().loadInstance(getRepository(), getVersion());
|
||||
resourcePackTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (worldListTab.isInitialized())
|
||||
worldListTab.getNode().loadInstance(getRepository(), getVersion());
|
||||
worldListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (schematicsTab.isInitialized())
|
||||
schematicsTab.getNode().loadInstance(getRepository(), getVersion());
|
||||
schematicsTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
});
|
||||
|
||||
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
|
||||
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> checkSelectedInstance(), EventPriority.HIGHEST));
|
||||
}
|
||||
|
||||
private void checkSelectedVersion() {
|
||||
private void checkSelectedInstance() {
|
||||
runInFX(() -> {
|
||||
if (this.instanceReference.get() == null) return;
|
||||
HMCLGameRepository repository = this.instanceReference.get().repository();
|
||||
if (!repository.hasVersion(this.instanceReference.get().instanceId())) {
|
||||
if (preferredVersionName != null) {
|
||||
loadVersion(preferredVersionName, repository);
|
||||
@Nullable GameInstanceID instanceId = this.instanceReference.get().instanceId();
|
||||
if (instanceId == null || !repository.hasInstance(instanceId)) {
|
||||
if (preferredInstanceId != null) {
|
||||
loadInstance(preferredInstanceId, repository);
|
||||
} else {
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
@@ -122,12 +124,13 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
});
|
||||
}
|
||||
|
||||
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
private <T extends Node> Supplier<T> loadInstanceFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (instanceReference.get() != null) {
|
||||
if (node instanceof VersionPage.GameInstanceLoadable) {
|
||||
((GameInstanceLoadable) node).loadInstance(instanceReference.get().repository(), instanceReference.get().instanceId());
|
||||
if (node instanceof GameInstancePage.GameInstanceLoadable loadable) {
|
||||
@Nullable GameInstanceID instanceId = instanceReference.get().instanceId();
|
||||
loadable.loadInstance(instanceReference.get().repository(), instanceId);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
@@ -135,38 +138,38 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
public void showInstanceSettings() {
|
||||
tab.select(versionSettingsTab, false);
|
||||
tab.select(gameSettingsTab, false);
|
||||
}
|
||||
|
||||
public void setVersion(String version, HMCLGameRepository repository) {
|
||||
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, version));
|
||||
public void setInstance(GameInstanceID instanceId, HMCLGameRepository repository) {
|
||||
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId));
|
||||
}
|
||||
|
||||
public void loadVersion(String version, HMCLGameRepository repository) {
|
||||
public void loadInstance(GameInstanceID instanceId, HMCLGameRepository repository) {
|
||||
// If we jumped to game list page and deleted this version
|
||||
// and back to this page, we should return to main page.
|
||||
if (this.instanceReference.get() != null && (!getRepository().isLoaded() ||
|
||||
!getRepository().hasVersion(version))) {
|
||||
!getRepository().hasInstance(instanceId))) {
|
||||
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
|
||||
return;
|
||||
}
|
||||
|
||||
setVersion(version, repository);
|
||||
preferredVersionName = version;
|
||||
setInstance(instanceId, repository);
|
||||
preferredInstanceId = instanceId;
|
||||
|
||||
if (versionSettingsTab.isInitialized())
|
||||
versionSettingsTab.getNode().loadInstance(repository, version);
|
||||
if (gameSettingsTab.isInitialized())
|
||||
gameSettingsTab.getNode().loadInstance(repository, instanceId);
|
||||
if (installerListTab.isInitialized())
|
||||
installerListTab.getNode().loadInstance(repository, version);
|
||||
installerListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (modListTab.isInitialized())
|
||||
modListTab.getNode().loadInstance(repository, version);
|
||||
modListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (resourcePackTab.isInitialized())
|
||||
resourcePackTab.getNode().loadInstance(repository, version);
|
||||
resourcePackTab.getNode().loadInstance(repository, instanceId);
|
||||
if (worldListTab.isInitialized())
|
||||
worldListTab.getNode().loadInstance(repository, version);
|
||||
worldListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (schematicsTab.isInitialized())
|
||||
schematicsTab.getNode().loadInstance(repository, version);
|
||||
currentVersionUpgradable.set(repository.isModpack(version));
|
||||
schematicsTab.getNode().loadInstance(repository, instanceId);
|
||||
currentInstanceUpgradable.set(repository.isModpack(instanceId));
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
@@ -176,20 +179,20 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
// If we jumped to game list page and deleted this version
|
||||
// and back to this page, we should return to main page.
|
||||
if (!getRepository().isLoaded() ||
|
||||
!getRepository().hasVersion(getVersion())) {
|
||||
!getRepository().hasInstance(getInstanceId())) {
|
||||
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
|
||||
return;
|
||||
}
|
||||
|
||||
loadVersion(getVersion(), getRepository());
|
||||
loadInstance(getInstanceId(), getRepository());
|
||||
}
|
||||
|
||||
private void onBrowse(String sub) {
|
||||
FXUtils.openFolder(getRepository().getRunDirectory(getVersion()).resolve(sub));
|
||||
FXUtils.openFolder(getRepository().getRunDirectory(getInstanceId()).resolve(sub));
|
||||
}
|
||||
|
||||
private void redownloadAssetIndex() {
|
||||
Versions.updateGameAssets(getRepository(), getVersion());
|
||||
Instances.updateGameAssets(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void clearLibraries() {
|
||||
@@ -224,44 +227,46 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
Versions.cleanVersion(getRepository(), getVersion());
|
||||
Instances.cleanInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void testGame() {
|
||||
Versions.testGame(getRepository(), getVersion());
|
||||
Instances.testGame(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void updateGame() {
|
||||
Versions.updateVersion(getRepository(), getVersion());
|
||||
Instances.updateInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void generateLaunchScript() {
|
||||
Versions.generateLaunchScript(getRepository(), getVersion());
|
||||
Instances.generateLaunchScript(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void export() {
|
||||
Versions.exportVersion(getRepository(), getVersion());
|
||||
Instances.exportInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void rename() {
|
||||
Versions.renameVersion(getRepository(), getVersion())
|
||||
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
|
||||
Instances.renameInstance(getRepository(), getInstanceId())
|
||||
.thenApply(newInstanceId -> this.preferredInstanceId = new GameInstanceID(newInstanceId));
|
||||
}
|
||||
|
||||
private void remove() {
|
||||
Versions.deleteVersion(getRepository(), getVersion());
|
||||
Instances.deleteInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
private void duplicate() {
|
||||
Versions.duplicateVersion(getRepository(), getVersion());
|
||||
Instances.duplicateInstance(getRepository(), getInstanceId());
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::repository).orElse(null);
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::instanceId).orElse(null);
|
||||
public @Nullable GameInstanceID getInstanceId() {
|
||||
return Optional.ofNullable(instanceReference.get())
|
||||
.map(HMCLGameRepository.InstanceReference::instanceId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -274,19 +279,19 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static class Skin extends DecoratorAnimatedPageSkin<VersionPage> {
|
||||
public static class Skin extends DecoratorAnimatedPageSkin<GameInstancePage> {
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
protected Skin(VersionPage control) {
|
||||
protected Skin(GameInstancePage control) {
|
||||
super(control);
|
||||
|
||||
{
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.gameSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE)
|
||||
@@ -312,25 +317,25 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
PopupMenu managementList = new PopupMenu();
|
||||
JFXPopup managementPopup = new JFXPopup(managementList);
|
||||
managementList.getContent().setAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), control::testGame, managementPopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), control::generateLaunchScript, managementPopup),
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), control::testGame, managementPopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), control::generateLaunchScript, managementPopup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), control::rename, managementPopup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), control::duplicate, managementPopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), control::remove, managementPopup),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("instance.manage.rename"), control::rename, managementPopup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("instance.manage.duplicate"), control::duplicate, managementPopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), control::remove, managementPopup),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), control::export, managementPopup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove_assets"), control::clearAssets, managementPopup),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove_libraries"), control::clearLibraries, managementPopup),
|
||||
new IconedMenuItem(null, i18n("version.manage.clean"), control::clearJunkFiles, managementPopup).addTooltip(i18n("version.manage.clean.tooltip"))
|
||||
new IconedMenuItem(null, i18n("instance.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup),
|
||||
new IconedMenuItem(null, i18n("instance.manage.remove_assets"), control::clearAssets, managementPopup),
|
||||
new IconedMenuItem(null, i18n("instance.manage.remove_libraries"), control::clearLibraries, managementPopup),
|
||||
new IconedMenuItem(null, i18n("instance.manage.clean"), control::clearJunkFiles, managementPopup).addTooltip(i18n("instance.manage.clean.tooltip"))
|
||||
);
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(i18n("version.update"), SVG.UPDATE, control::updateGame, upgradeItem -> {
|
||||
upgradeItem.visibleProperty().bind(control.currentVersionUpgradable);
|
||||
.addNavigationDrawerItem(i18n("instance.update"), SVG.UPDATE, control::updateGame, upgradeItem -> {
|
||||
upgradeItem.visibleProperty().bind(control.currentInstanceUpgradable);
|
||||
})
|
||||
.addNavigationDrawerItem(i18n("version.launch.test"), SVG.ROCKET_LAUNCH, control::testGame)
|
||||
.addNavigationDrawerItem(i18n("instance.launch.test"), SVG.ROCKET_LAUNCH, control::testGame)
|
||||
.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, null, browseMenuItem -> {
|
||||
browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0));
|
||||
})
|
||||
@@ -344,7 +349,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
control.state.bind(Bindings.createObjectBinding(() ->
|
||||
State.fromTitle(i18n("version.manage.manage.title", getSkinnable().getVersion()), -1),
|
||||
State.fromTitle(i18n("instance.manage.manage.title", getSkinnable().getInstanceId()), -1),
|
||||
getSkinnable().instanceReference));
|
||||
|
||||
//control.transitionPane.getStyleClass().add("gray-background");
|
||||
@@ -359,6 +364,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
///
|
||||
/// @param repository the repository containing the game instance
|
||||
/// @param instanceId the game instance ID, or `null` when only repository context is available
|
||||
void loadInstance(HMCLGameRepository repository, @Nullable String instanceId);
|
||||
void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId);
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -15,11 +15,12 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
@@ -44,6 +45,7 @@ public class GameItem {
|
||||
|
||||
protected final HMCLGameRepository repository;
|
||||
protected final String id;
|
||||
protected final GameInstanceID instanceId;
|
||||
|
||||
private boolean initialized = false;
|
||||
private StringProperty title;
|
||||
@@ -51,9 +53,10 @@ public class GameItem {
|
||||
private StringProperty subtitle;
|
||||
private ObjectProperty<Image> image;
|
||||
|
||||
public GameItem(HMCLGameRepository repository, String id) {
|
||||
public GameItem(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.id = id;
|
||||
this.id = instanceId.toString();
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
public GameDirectory getGameDirectory() {
|
||||
@@ -83,10 +86,10 @@ public class GameItem {
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
|
||||
Optional<String> gameVersion = repository.getGameVersion(id);
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
String modPackVersion = null;
|
||||
try {
|
||||
ModpackConfiguration<?> config = repository.readModpackConfiguration(id);
|
||||
ModpackConfiguration<?> config = repository.readModpackConfiguration(instanceId);
|
||||
modPackVersion = config != null ? config.getVersion() : null;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read modpack configuration from " + id, e);
|
||||
@@ -99,7 +102,7 @@ public class GameItem {
|
||||
}
|
||||
|
||||
StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown")));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(id), result.gameVersion);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), result.gameVersion);
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
@@ -118,7 +121,7 @@ public class GameItem {
|
||||
}, Schedulers.javafx());
|
||||
|
||||
title.set(id);
|
||||
image.set(repository.getVersionIconImage(id));
|
||||
image.set(repository.getInstanceIconImage(instanceId));
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
+11
-10
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -29,6 +29,7 @@ import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
@@ -69,7 +70,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
fireEvent(new ActionEvent());
|
||||
GameListItem item = GameListCell.this.getItem();
|
||||
if (item != null) {
|
||||
item.getRepository().setSelectedInstance(item.getId());
|
||||
item.getRepository().setSelectedInstance(new GameInstanceID(item.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +114,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
if (item != null)
|
||||
item.update();
|
||||
});
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("instance.update"));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
|
||||
this.btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH);
|
||||
@@ -123,7 +124,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
item.testGame();
|
||||
});
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch.test"));
|
||||
right.getChildren().add(btnLaunch);
|
||||
|
||||
this.btnManage = FXUtils.newToggleButton4(SVG.MORE_VERT);
|
||||
@@ -191,14 +192,14 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
|
||||
menu.getContent().setAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), item::testGame, popup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), item::testGame, popup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), item::generateLaunchScript, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), item::modifyGameSettings, popup),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("instance.manage.manage"), item::modifyGameSettings, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), item::rename, popup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), item::duplicate, popup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), item::remove, popup),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("instance.manage.rename"), item::rename, popup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("instance.manage.duplicate"), item::duplicate, popup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), item::remove, popup),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), item::export, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), item::browse, popup));
|
||||
+19
-15
@@ -15,12 +15,13 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
|
||||
@@ -30,11 +31,14 @@ public class GameListItem extends GameItem {
|
||||
private final boolean isModpack;
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
public GameListItem(HMCLGameRepository repository, String id) {
|
||||
super(repository, id);
|
||||
this.isModpack = repository.isModpack(id);
|
||||
public GameListItem(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
super(repository, instanceId);
|
||||
this.isModpack = repository.isModpack(instanceId);
|
||||
selected.bind(Bindings.createBooleanBinding(
|
||||
() -> repository.getGameDirectory() == GameDirectoryManager.getSelectedGameDirectory() && Objects.equals(repository.getSelectedInstance(), id),
|
||||
() -> {
|
||||
if (repository.getGameDirectory() != GameDirectoryManager.getSelectedGameDirectory()) return false;
|
||||
return Objects.equals(repository.getSelectedInstance(), instanceId);
|
||||
},
|
||||
GameDirectoryManager.selectedGameDirectoryProperty(),
|
||||
GameDirectoryManager.selectedInstanceProperty()));
|
||||
}
|
||||
@@ -44,39 +48,39 @@ public class GameListItem extends GameItem {
|
||||
}
|
||||
|
||||
public void rename() {
|
||||
Versions.renameVersion(repository, id);
|
||||
Instances.renameInstance(repository, instanceId);
|
||||
}
|
||||
|
||||
public void duplicate() {
|
||||
Versions.duplicateVersion(repository, id);
|
||||
Instances.duplicateInstance(repository, instanceId);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
Versions.deleteVersion(repository, id);
|
||||
Instances.deleteInstance(repository, instanceId);
|
||||
}
|
||||
|
||||
public void export() {
|
||||
Versions.exportVersion(repository, id);
|
||||
Instances.exportInstance(repository, instanceId);
|
||||
}
|
||||
|
||||
public void browse() {
|
||||
Versions.openFolder(repository, id);
|
||||
Instances.openFolder(repository, instanceId);
|
||||
}
|
||||
|
||||
public void testGame() {
|
||||
Versions.testGame(repository, id);
|
||||
Instances.testGame(repository, instanceId);
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
Versions.launch(repository, id);
|
||||
Instances.launch(repository, instanceId);
|
||||
}
|
||||
|
||||
public void modifyGameSettings() {
|
||||
Versions.modifyGameSettings(repository, id);
|
||||
Instances.modifyGameSettings(repository, instanceId);
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScript(repository, id);
|
||||
Instances.generateLaunchScript(repository, instanceId);
|
||||
}
|
||||
|
||||
public boolean canUpdate() {
|
||||
@@ -84,6 +88,6 @@ public class GameListItem extends GameItem {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
Versions.updateVersion(repository, id);
|
||||
Instances.updateInstance(repository, instanceId);
|
||||
}
|
||||
}
|
||||
+40
-16
@@ -15,20 +15,24 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
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.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
@@ -51,10 +55,11 @@ 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.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@@ -70,7 +75,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("instance.manage")));
|
||||
/// Navigation drawer items for configured game directories.
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final ObservableList<GameDirectoryListItem> gameDirectoryListItems;
|
||||
@@ -101,12 +106,11 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
wrapper.getChildren().setAll(box, addGameDirectoryItem);
|
||||
pane.setContent(wrapper);
|
||||
}
|
||||
FXUtils.smoothScrolling(pane);
|
||||
|
||||
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 * 3 + 12 * 2);
|
||||
FXUtils.setLimitHeight(bottomLeftCornerList, 40 + 12 * 2);
|
||||
setLeft(pane, bottomLeftCornerList);
|
||||
}
|
||||
|
||||
@@ -118,13 +122,13 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file), i18n("install.modpack"));
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Versions.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
|
||||
Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void modifyGlobalGameSettings() {
|
||||
Versions.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository());
|
||||
Instances.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,7 +147,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
GameDirectoryManager.registerVersionsListener(this::loadVersions);
|
||||
|
||||
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
setOnFailedAction(e -> Instances.addNewGame());
|
||||
}
|
||||
|
||||
@FXThread
|
||||
@@ -152,12 +156,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
setLoading(true);
|
||||
setFailedReason(null);
|
||||
|
||||
List<GameListItem> versionItems = repository.getDisplayVersions().map(instance -> new GameListItem(repository, instance.getId())).toList();
|
||||
List<GameListItem> versionItems = repository.getDisplayInstanceManifests().map(instance -> new GameListItem(repository, instance.id())).toList();
|
||||
|
||||
sourceList.setAll(versionItems);
|
||||
|
||||
if (versionItems.isEmpty()) {
|
||||
setFailedReason(i18n("version.empty.hint"));
|
||||
setFailedReason(i18n("instance.empty.hint"));
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -182,7 +186,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
public void refreshList() {
|
||||
GameDirectoryManager.getSelectedRepository().refreshVersionsAsync().start();
|
||||
GameDirectoryManager.getSelectedRepository().refreshAsync().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -195,6 +199,9 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private final HBox searchBar;
|
||||
private final HBox toolbarNormal;
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
private final JFXTextField searchField;
|
||||
|
||||
public GameListSkin(GameList skinnable) {
|
||||
@@ -222,20 +229,26 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
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);
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
|
||||
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("install.new_game"), SVG.DOWNLOAD, Instances::addNewGame), createToolbarButton2(i18n("install.modpack"), SVG.PACKAGE2, Instances::importModpack), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
|
||||
|
||||
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
|
||||
|
||||
@@ -255,6 +268,15 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.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);
|
||||
}
|
||||
@@ -269,6 +291,8 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
runInFX(searchField::requestFocus);
|
||||
|
||||
isSearching.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-10
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
@@ -33,8 +33,10 @@ import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
@@ -48,22 +50,38 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/// @author Glavo
|
||||
public final class GameListPopupMenu extends StackPane {
|
||||
|
||||
private static final String KEY = GameListPopupMenu.class.getName() + ".popup";
|
||||
|
||||
public static boolean hideShowing(Node owner) {
|
||||
JFXPopup popup = (JFXPopup) owner.getProperties().get(KEY);
|
||||
if (popup != null && popup.isShowing()) {
|
||||
popup.hide();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows an instance selection popup relative to its owner.
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<Version> versions) {
|
||||
HMCLGameRepository repository, List<GameInstanceManifest> versions) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
|
||||
}
|
||||
|
||||
/// Shows and returns an instance selection popup relative to its owner.
|
||||
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<Version> versions) {
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<GameInstanceManifest> versions) {
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream()
|
||||
.filter(it -> repository.hasVersion(it.getId()))
|
||||
.map(it -> new GameItem(repository, it.getId()))
|
||||
.filter(it -> repository.hasInstance(it.id()))
|
||||
.map(it -> new GameItem(repository, it.id()))
|
||||
.toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
owner.getProperties().put(KEY, popup);
|
||||
popup.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> owner.getProperties().remove(KEY, popup));
|
||||
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY, true);
|
||||
|
||||
return popup;
|
||||
}
|
||||
@@ -82,7 +100,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
|
||||
listView.prefHeightProperty().bind(Bindings.size(getItems()).multiply(50).add(2));
|
||||
|
||||
Label placeholder = new Label(i18n("version.empty"));
|
||||
Label placeholder = new Label(i18n("instance.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
@@ -135,7 +153,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
FXUtils.onClicked(rootPane, () -> {
|
||||
GameItem item = getItem();
|
||||
if (item != null) {
|
||||
item.getRepository().setSelectedInstance(item.getId());
|
||||
item.getRepository().setSelectedInstance(new GameInstanceID(item.getId()));
|
||||
if (getScene().getWindow() instanceof JFXPopup popup)
|
||||
popup.hide();
|
||||
}
|
||||
+15
-30
@@ -15,8 +15,9 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.game.LocalizedRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
@@ -30,42 +31,26 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS);
|
||||
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, null);
|
||||
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, null, ModrinthRemoteAddonRepository.MODS);
|
||||
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofCurseForgeResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, null);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModrinthResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, null, ModrinthRemoteAddonRepository.RESOURCE_PACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS);
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS);
|
||||
page.supportChinese.set(false);
|
||||
return page;
|
||||
}
|
||||
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteAddonRepository.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
super(null, callback, versionSelection);
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean instanceSelection, RemoteAddon.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
super(null, callback, instanceSelection);
|
||||
|
||||
repository = new Repository(type, curseForge, modrinth);
|
||||
|
||||
@@ -98,11 +83,11 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteAddonRepository {
|
||||
private final RemoteAddonRepository.Type type;
|
||||
private final RemoteAddon.Type type;
|
||||
private final CurseForgeRemoteAddonRepository curseForge;
|
||||
private final ModrinthRemoteAddonRepository modrinth;
|
||||
|
||||
public Repository(Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
public Repository(RemoteAddon.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) {
|
||||
this.type = type;
|
||||
this.curseForge = curseForge;
|
||||
this.modrinth = modrinth;
|
||||
@@ -127,7 +112,7 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
public RemoteAddon.Type getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
+23
-21
@@ -15,15 +15,16 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
@@ -32,6 +33,7 @@ import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
@@ -42,10 +44,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> implements VersionPage.GameInstanceLoadable {
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> implements GameInstancePage.GameInstanceLoadable {
|
||||
private HMCLGameRepository repository;
|
||||
private String versionId;
|
||||
private Version version;
|
||||
private GameInstanceID instanceId;
|
||||
private GameInstanceManifest manifest;
|
||||
private String gameVersion;
|
||||
|
||||
{
|
||||
@@ -61,16 +63,16 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.versionId = instanceId;
|
||||
this.version = repository.getVersion(instanceId);
|
||||
this.instanceId = instanceId;
|
||||
this.manifest = repository.getInstanceManifest(instanceId);
|
||||
this.gameVersion = null;
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
gameVersion = repository.getGameVersion(version).orElse(null);
|
||||
gameVersion = repository.getGameVersion(manifest).orElse(null);
|
||||
|
||||
return LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(instanceId), gameVersion);
|
||||
return LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion);
|
||||
}).thenAcceptAsync(analyzer -> {
|
||||
itemsProperty().clear();
|
||||
|
||||
@@ -98,13 +100,13 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
}
|
||||
|
||||
item.setOnInstall(() -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, version, libraryId, libraryVersion));
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, manifest, libraryId, libraryVersion));
|
||||
});
|
||||
|
||||
item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId)
|
||||
item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.withComposeAsync(repository.refreshVersionsAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId))
|
||||
.withComposeAsync(repository.refreshAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId))
|
||||
.start());
|
||||
|
||||
itemsProperty().add(item);
|
||||
@@ -123,10 +125,10 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId)
|
||||
installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.withComposeAsync(repository.refreshVersionsAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId))
|
||||
.withComposeAsync(repository.refreshAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId))
|
||||
.start());
|
||||
|
||||
itemsProperty().add(installerItem);
|
||||
@@ -137,21 +139,21 @@ 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 = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
Path file = Controllers.showOpenDialog(chooser);
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
private void doInstallOffline(Path file) {
|
||||
Task<?> task = repository.getDependency().installLibraryAsync(version, file)
|
||||
Task<?> task = repository.getDependency().installLibraryAsync(manifest, file)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.thenComposeAsync(repository.refreshVersionsAsync());
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
runInFX(() -> {
|
||||
if (success) {
|
||||
loadInstance(repository, versionId);
|
||||
loadInstance(repository, instanceId);
|
||||
Controllers.dialog(i18n("install.success"));
|
||||
} else {
|
||||
if (executor.getException() == null)
|
||||
+101
-82
@@ -15,13 +15,12 @@
|
||||
* 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.versions;
|
||||
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
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;
|
||||
@@ -29,8 +28,11 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.QuickPlayOption;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -47,6 +49,7 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
@@ -62,8 +65,8 @@ import java.util.function.Consumer;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Versions {
|
||||
private Versions() {
|
||||
public final class Instances {
|
||||
private Instances() {
|
||||
}
|
||||
|
||||
public static void addNewGame() {
|
||||
@@ -78,7 +81,7 @@ public final class Versions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, String version, RemoteAddon mod, RemoteAddon.Version file) {
|
||||
public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, GameInstanceID instanceId, RemoteAddon mod, RemoteAddon.Version file) {
|
||||
Path modpack;
|
||||
List<URI> downloadURLs;
|
||||
try {
|
||||
@@ -92,11 +95,12 @@ public final class Versions {
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(downloadURLs, modpack)
|
||||
.setName(file.name())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
ModpackInstallWizardProvider installWizardProvider;
|
||||
if (version != null)
|
||||
installWizardProvider = new ModpackInstallWizardProvider(repository, modpack, version);
|
||||
if (instanceId != null)
|
||||
installWizardProvider = new ModpackInstallWizardProvider(repository, modpack, instanceId);
|
||||
else
|
||||
installWizardProvider = new ModpackInstallWizardProvider(repository, modpack);
|
||||
if (StringUtils.isNotBlank(mod.iconUrl()))
|
||||
@@ -115,19 +119,19 @@ public final class Versions {
|
||||
);
|
||||
}
|
||||
|
||||
public static void deleteVersion(HMCLGameRepository repository, String version) {
|
||||
boolean isIndependent = repository.getRunDirectory(version).toAbsolutePath().normalize()
|
||||
.equals(repository.getVersionRoot(version).toAbsolutePath().normalize());
|
||||
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
|
||||
i18n("version.manage.remove.confirm.trash", version, version + "_removed");
|
||||
public static void deleteInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
boolean isIndependent = repository.getRunDirectory(instanceId).toAbsolutePath().normalize()
|
||||
.equals(repository.getInstanceRoot(instanceId).toAbsolutePath().normalize());
|
||||
String message = isIndependent ? i18n("instance.manage.remove.confirm.independent", instanceId) :
|
||||
i18n("instance.manage.remove.confirm.trash", instanceId, instanceId + "_removed");
|
||||
|
||||
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
|
||||
deleteButton.getStyleClass().add("dialog-error");
|
||||
deleteButton.setOnAction(e -> {
|
||||
Task.supplyAsync(Schedulers.io(), () -> repository.removeVersionFromDisk(version))
|
||||
Task.supplyAsync(Schedulers.io(), () -> repository.removeInstanceFromDisk(instanceId))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || !Boolean.TRUE.equals(result)) {
|
||||
Controllers.dialog(i18n("version.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Controllers.dialog(i18n("instance.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
@@ -135,50 +139,57 @@ public final class Versions {
|
||||
Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton);
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> renameVersion(HMCLGameRepository repository, String version) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
|
||||
if (newName.equals(version)) {
|
||||
public static CompletableFuture<String> renameInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
return Controllers.prompt(i18n("instance.manage.rename.message"), (newName, handler) -> {
|
||||
if (newName.equals(instanceId.toString())) {
|
||||
handler.resolve();
|
||||
return;
|
||||
}
|
||||
if (repository.renameVersion(version, newName)) {
|
||||
GameInstanceID oldInstanceId = instanceId;
|
||||
GameInstanceID newInstanceId = new GameInstanceID(newName);
|
||||
if (repository.renameInstance(oldInstanceId, newInstanceId)) {
|
||||
handler.resolve();
|
||||
repository.refreshVersionsAsync()
|
||||
repository.refreshAsync()
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
if (repository.hasVersion(newName)) {
|
||||
repository.setSelectedInstance(newName);
|
||||
if (repository.hasInstance(newInstanceId)) {
|
||||
repository.setSelectedInstance(newInstanceId);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
handler.reject(i18n("version.manage.rename.fail"));
|
||||
handler.reject(i18n("instance.manage.rename.fail"));
|
||||
}
|
||||
}, version,
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||
}, instanceId.toString(),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName) || newVersionName.equals(instanceId.toString())));
|
||||
}
|
||||
|
||||
public static void exportVersion(HMCLGameRepository repository, String version) {
|
||||
Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, version), i18n("modpack.wizard"));
|
||||
public static void exportInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, instanceId), i18n("modpack.wizard"));
|
||||
}
|
||||
|
||||
public static void openFolder(HMCLGameRepository repository, String version) {
|
||||
FXUtils.openFolder(repository.getRunDirectory(version));
|
||||
public static void openFolder(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
FXUtils.openFolder(repository.getRunDirectory(instanceId));
|
||||
}
|
||||
|
||||
public static void installFromJson(HMCLGameRepository repository, Path file) {
|
||||
Version version;
|
||||
GameInstanceManifest manifest;
|
||||
try {
|
||||
version = repository.readVersionJson(file);
|
||||
manifest = JsonUtils.fromJsonFile(file, GameInstanceManifest.class);
|
||||
if (manifest == null) {
|
||||
throw new IllegalArgumentException("Missing game manifest");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
|
||||
Controllers.prompt(i18n("instance.manage.duplicate.prompt"), (result, handler) -> {
|
||||
handler.resolve();
|
||||
|
||||
GameInstanceID instanceId = new GameInstanceID(result);
|
||||
|
||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
||||
Version newVersion = version.setId(result).setJar(result);
|
||||
GameInstanceManifest newVersion = manifest.withId(instanceId).withJar(instanceId);
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
|
||||
@@ -189,71 +200,72 @@ public final class Versions {
|
||||
// ignore failure
|
||||
}))
|
||||
.thenComposeAsync(repository.saveAsync(newVersion))
|
||||
.thenRunAsync(repository::refreshVersions)
|
||||
.thenRunAsync(repository::refresh)
|
||||
.whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception == null) {
|
||||
repository.setSelectedInstance(result);
|
||||
repository.setSelectedInstance(new GameInstanceID(result));
|
||||
} else {
|
||||
Controllers.dialog(
|
||||
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName)));
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName)));
|
||||
}
|
||||
|
||||
public static void duplicateVersion(HMCLGameRepository repository, String version) {
|
||||
public static void duplicateInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.prompt(
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
new PromptDialogPane.Builder(i18n("instance.manage.duplicate.prompt"), (res, handler) -> {
|
||||
String newInstanceName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
GameInstanceID newInstanceId = new GameInstanceID(newInstanceName);
|
||||
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
||||
Task.runAsync(() -> repository.duplicateVersion(version, newVersionName, copySaves))
|
||||
.thenComposeAsync(repository.refreshVersionsAsync())
|
||||
Task.runAsync(() -> repository.duplicateInstance(instanceId, newInstanceId, copySaves))
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
handler.resolve();
|
||||
} else {
|
||||
handler.reject(StringUtils.getStackTrace(exception));
|
||||
if (!repository.versionIdConflicts(newVersionName)) {
|
||||
repository.removeVersionFromDisk(newVersionName);
|
||||
if (!repository.instanceIdConflicts(newInstanceId)) {
|
||||
repository.removeInstanceFromDisk(newInstanceId);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName))))
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("instance.manage.duplicate.confirm")))
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, instanceId.toString(),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName))))
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
public static void updateVersion(HMCLGameRepository repository, String version) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, version));
|
||||
public static void updateInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, instanceId));
|
||||
}
|
||||
|
||||
public static void updateGameAssets(HMCLGameRepository repository, String version) {
|
||||
TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
|
||||
public static void updateGameAssets(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getInstanceManifest(instanceId), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
|
||||
.executor();
|
||||
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL);
|
||||
Controllers.taskDialog(executor, i18n("instance.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL);
|
||||
executor.start();
|
||||
}
|
||||
|
||||
public static void cleanVersion(HMCLGameRepository repository, String id) {
|
||||
public static void cleanInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
try {
|
||||
repository.clean(id);
|
||||
repository.clean(instanceId);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to clean game directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void generateLaunchScript(HMCLGameRepository repository, String id, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, id))
|
||||
public static void generateLaunchScript(HMCLGameRepository repository, GameInstanceID instanceId, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, instanceId))
|
||||
return;
|
||||
ensureSelectedAccount(account -> {
|
||||
FileChooser chooser = new FileChooser();
|
||||
if (Files.isDirectory(repository.getRunDirectory(id)))
|
||||
chooser.setInitialDirectory(repository.getRunDirectory(id).toFile());
|
||||
chooser.setTitle(i18n("version.launch_script.save"));
|
||||
if (Files.isDirectory(repository.getRunDirectory(instanceId)))
|
||||
chooser.setInitialDirectory(repository.getRunDirectory(instanceId).toFile());
|
||||
chooser.setTitle(i18n("instance.launch_script.save"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
chooser.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter(i18n("extension.command"), "*.command")
|
||||
@@ -263,14 +275,14 @@ 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 = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
|
||||
Path file = Controllers.showSaveDialog(chooser);
|
||||
if (file != null) {
|
||||
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
|
||||
String defaultExt = getDefaultScriptExtension();
|
||||
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
|
||||
}
|
||||
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, id);
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
}
|
||||
@@ -295,11 +307,11 @@ public final class Versions {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void launch(HMCLGameRepository repository, String id, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, id))
|
||||
public static void launch(HMCLGameRepository repository, GameInstanceID instanceId, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, instanceId))
|
||||
return;
|
||||
ensureSelectedAccount(account -> {
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, id);
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
}
|
||||
@@ -307,27 +319,34 @@ public final class Versions {
|
||||
});
|
||||
}
|
||||
|
||||
public static void testGame(HMCLGameRepository repository, String id) {
|
||||
launch(repository, id, LauncherHelper::setTestMode);
|
||||
public static void testGame(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
launch(repository, instanceId, LauncherHelper::setTestMode);
|
||||
}
|
||||
|
||||
public static void launchAndEnterWorld(HMCLGameRepository repository, String id, String worldFolderName) {
|
||||
launch(repository, id, launcherHelper ->
|
||||
public static void launchAndEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) {
|
||||
launch(repository, instanceId, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, String id, String worldFolderName) {
|
||||
generateLaunchScript(repository, id, launcherHelper ->
|
||||
public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) {
|
||||
generateLaunchScript(repository, instanceId, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
private static boolean checkVersionForLaunching(HMCLGameRepository repository, String id) {
|
||||
if (id == null || !repository.isLoaded() || !repository.hasVersion(id)) {
|
||||
JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download"));
|
||||
private static boolean checkVersionForLaunching(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
boolean unavailable;
|
||||
if (instanceId == null || !repository.isLoaded()) {
|
||||
unavailable = true;
|
||||
} else {
|
||||
unavailable = !repository.hasInstance(instanceId);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download"));
|
||||
gotoDownload.getStyleClass().add("dialog-accept");
|
||||
gotoDownload.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
|
||||
Controllers.confirmAction(i18n("version.empty.launch"), i18n("launch.failed"),
|
||||
Controllers.confirmAction(i18n("instance.empty.launch"), i18n("launch.failed"),
|
||||
MessageDialogPane.MessageType.ERROR,
|
||||
gotoDownload,
|
||||
null);
|
||||
@@ -372,10 +391,10 @@ public final class Versions {
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
|
||||
public static void modifyGameSettings(HMCLGameRepository repository, String version) {
|
||||
Controllers.getVersionPage().setVersion(version, repository);
|
||||
Controllers.getVersionPage().showInstanceSettings();
|
||||
public static void modifyGameSettings(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getGameInstancePage().setInstance(instanceId, repository);
|
||||
Controllers.getGameInstancePage().showInstanceSettings();
|
||||
// VersionPage.loadVersion will be invoked after navigation
|
||||
Controllers.navigate(Controllers.getVersionPage());
|
||||
Controllers.navigate(Controllers.getGameInstancePage());
|
||||
}
|
||||
}
|
||||
+24
-28
@@ -15,16 +15,15 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
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.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
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;
|
||||
@@ -39,6 +38,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -51,14 +51,12 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
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);
|
||||
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements GameInstancePage.GameInstanceLoadable, PageAware {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private ModManager modManager;
|
||||
private HMCLGameRepository repository;
|
||||
private String instanceId;
|
||||
private GameInstanceID instanceId;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
@@ -86,20 +84,22 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
|
||||
Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId);
|
||||
GameInstanceManifest resolved = repository.getResolvedInstanceManifest(instanceId).standaloneManifest();
|
||||
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();
|
||||
@@ -112,6 +112,10 @@ 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) {
|
||||
@@ -172,8 +176,8 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
|
||||
if (res == null) return;
|
||||
|
||||
@@ -236,6 +240,10 @@ 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);
|
||||
@@ -265,7 +273,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void download() {
|
||||
Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
|
||||
Controllers.getDownloadPage().showModDownloads().selectInstance(instanceId);
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -278,18 +286,6 @@ 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();
|
||||
}
|
||||
@@ -298,7 +294,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
public String getInstanceId() {
|
||||
public GameInstanceID getInstanceId() {
|
||||
return this.instanceId;
|
||||
}
|
||||
}
|
||||
+67
-62
@@ -15,14 +15,16 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
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;
|
||||
@@ -35,16 +37,15 @@ 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.*;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
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;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -61,6 +62,7 @@ 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;
|
||||
@@ -80,6 +82,7 @@ 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;
|
||||
@@ -88,10 +91,14 @@ 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;
|
||||
|
||||
@FXThread
|
||||
private boolean isSearching = false;
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -101,6 +108,7 @@ 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");
|
||||
@@ -118,19 +126,22 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> search());
|
||||
searchPause.setOnFinished(e -> search());
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
isSearching = false;
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
@@ -150,7 +161,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("mods.download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
);
|
||||
|
||||
@@ -192,7 +203,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching ? searchBar : toolbarNormal);
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
@@ -219,9 +230,26 @@ 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) {
|
||||
if (isSearching.get()) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
@@ -242,14 +270,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -264,7 +284,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching = true;
|
||||
isSearching.set(true);
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
@@ -343,7 +363,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
@@ -359,7 +379,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
@@ -379,11 +399,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
titleContainer.setPadding(new Insets(0, 0, 12, 0));
|
||||
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
DoubleBinding widthBinding = Controllers.getDecorator().contentWidthProperty().multiply(0.7);
|
||||
prefWidthProperty().bind(widthBinding);
|
||||
maxWidthProperty().bind(widthBinding);
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
@@ -392,13 +415,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
StringJoiner subtitle = new StringJoiner(" | ");
|
||||
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
StringJoiner subtitle = new StringJoiner("\n");
|
||||
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitle.add(modInfo.getModInfo().getGameVersion());
|
||||
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitle.add(modInfo.getModInfo().getVersion());
|
||||
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
@@ -418,7 +441,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
@@ -435,7 +458,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteAddon.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().modid());
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
@@ -457,15 +480,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
button.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(new DownloadPage(
|
||||
repository instanceof CurseForgeRemoteAddonRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteAddon,
|
||||
new HMCLGameRepository.InstanceReference(ModListPageSkin.this.getSkinnable().getRepository(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD
|
||||
));
|
||||
});
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
});
|
||||
}
|
||||
@@ -477,31 +492,21 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
|
||||
JFXHyperlink officialPageButton = new JFXHyperlink(i18n("mods.url"));
|
||||
officialPageButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(modInfo.getModInfo().getUrl());
|
||||
});
|
||||
|
||||
officialPageButton.setExternalLink(modInfo.getModInfo().getUrl());
|
||||
getActions().add(officialPageButton);
|
||||
}
|
||||
|
||||
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
|
||||
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
|
||||
searchButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
|
||||
pair("key", modInfo.getModInfo().getName()),
|
||||
pair("site", "all"),
|
||||
pair("filter", "0")
|
||||
)));
|
||||
});
|
||||
searchButton.setExternalLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
|
||||
pair("key", modInfo.getModInfo().getName()),
|
||||
pair("site", "all"),
|
||||
pair("filter", "0")
|
||||
)));
|
||||
getActions().add(searchButton);
|
||||
} else {
|
||||
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
|
||||
mcmodButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
});
|
||||
mcmodButton.setExternalLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
getActions().add(mcmodButton);
|
||||
}
|
||||
|
||||
@@ -543,7 +548,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
imageContainer.setImage(VersionIconType.COMMAND.getIcon());
|
||||
imageContainer.setImage(GameInstanceIconType.COMMAND.getIcon());
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
+4
-3
@@ -15,9 +15,9 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -56,7 +56,8 @@ public enum ModTranslations {
|
||||
}
|
||||
};
|
||||
|
||||
public static ModTranslations getTranslationsByRepositoryType(RemoteAddonRepository.Type type) {
|
||||
public static ModTranslations getTranslationsByAddonType(@Nullable RemoteAddon.Type type) {
|
||||
if (type == null) return EMPTY;
|
||||
return switch (type) {
|
||||
case MOD -> MOD;
|
||||
case MODPACK -> MODPACK;
|
||||
+50
-34
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
@@ -35,16 +35,18 @@ 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.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -57,7 +59,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.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -74,7 +76,7 @@ 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;
|
||||
|
||||
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> implements VersionPage.GameInstanceLoadable {
|
||||
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> implements GameInstancePage.GameInstanceLoadable {
|
||||
|
||||
private static final String TIP_KEY = "resourcePackWarning";
|
||||
private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) {
|
||||
@@ -89,7 +91,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
private HMCLGameRepository repository;
|
||||
private String instanceId;
|
||||
private GameInstanceID instanceId;
|
||||
|
||||
private Path resourcePackDirectory;
|
||||
private ResourcePackManager resourcePackManager;
|
||||
@@ -106,7 +108,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.resourcePackManager = new ResourcePackManager(repository, instanceId);
|
||||
@@ -176,14 +178,14 @@ 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 = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownload() {
|
||||
Controllers.getDownloadPage().showResourcePackDownloads().selectVersion(instanceId);
|
||||
Controllers.getDownloadPage().showResourcePackDownloads().selectInstance(instanceId);
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -257,6 +259,7 @@ 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,7 +269,11 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
private final HBox toolbarNormal = new HBox();
|
||||
private final HBox toolbarSelecting = new HBox();
|
||||
|
||||
private boolean isSearching;
|
||||
/// 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 ResourcePackListPageSkin(ResourcePackListPage control) {
|
||||
super(control);
|
||||
@@ -309,19 +316,22 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 5));
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> search());
|
||||
searchPause.setOnFinished(e -> search());
|
||||
FXUtils.onChange(searchField.textProperty(), newValue -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
isSearching = false;
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
@@ -339,14 +349,14 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () ->
|
||||
control.checkUpdates(listView.getItems().stream().map(ResourcePackInfoObject::getFile).toList())
|
||||
),
|
||||
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload),
|
||||
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
);
|
||||
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching ? searchBar : toolbarNormal);
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
@@ -371,6 +381,23 @@ 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 -> {
|
||||
@@ -403,7 +430,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching = true;
|
||||
isSearching.set(true);
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
@@ -567,8 +594,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
|
||||
ImageContainer imageContainer = new ImageContainer(40);
|
||||
imageContainer.setImage(packInfoObject.getIcon());
|
||||
@@ -594,7 +620,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
FXUtils.onChange(description.heightProperty(), newVal -> {
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
@@ -610,19 +636,9 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteAddon.Version> versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().modid());
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
button.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(new DownloadPage(
|
||||
repository instanceof CurseForgeRemoteAddonRepository
|
||||
? HMCLLocalizedDownloadListPage.ofCurseForgeResourcePack(null, false)
|
||||
: HMCLLocalizedDownloadListPage.ofModrinthResourcePack(null, false),
|
||||
remoteAddon,
|
||||
new HMCLGameRepository.InstanceReference(page.repository, page.instanceId),
|
||||
org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK
|
||||
));
|
||||
});
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
});
|
||||
}
|
||||
+11
-4
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
@@ -34,6 +34,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -62,7 +63,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements VersionPage.GameInstanceLoadable {
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements GameInstancePage.GameInstanceLoadable {
|
||||
|
||||
private static String translateAuthorName(String author) {
|
||||
if (I18n.isUseChinese() && "hsds".equals(author)) {
|
||||
@@ -87,7 +88,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.schematicsDirectory = repository.getSchematicsDirectory(instanceId);
|
||||
|
||||
refresh();
|
||||
@@ -149,7 +150,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 = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
}
|
||||
@@ -636,6 +637,12 @@ 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
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
+8
-1
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -162,6 +163,12 @@ 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
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
@@ -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 = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
|
||||
Path iconPath = Controllers.showOpenDialog(fileChooser);
|
||||
if (iconPath == null) return;
|
||||
|
||||
Image image;
|
||||
+19
-10
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
@@ -27,6 +27,7 @@ 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;
|
||||
@@ -35,6 +36,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -45,6 +47,7 @@ import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
@@ -61,13 +64,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class WorldListPage extends ListPageBase<World> implements VersionPage.GameInstanceLoadable {
|
||||
public final class WorldListPage extends ListPageBase<World> implements GameInstancePage.GameInstanceLoadable {
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
|
||||
private Path savesDir;
|
||||
private List<World> worlds;
|
||||
private HMCLGameRepository repository;
|
||||
private String instanceId;
|
||||
private GameInstanceID instanceId;
|
||||
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
|
||||
|
||||
private int refreshCount = 0;
|
||||
@@ -86,7 +89,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, String instanceId) {
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.savesDir = repository.getSavesDirectory(instanceId);
|
||||
@@ -140,7 +143,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 = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
|
||||
if (res == null || res.isEmpty()) return;
|
||||
installWorld(res.get(0));
|
||||
@@ -197,11 +200,11 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
public void launch(World world) {
|
||||
Versions.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript(World world) {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public BooleanProperty showAllProperty() {
|
||||
@@ -216,6 +219,12 @@ 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
|
||||
@@ -280,7 +289,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
|
||||
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
|
||||
right.getChildren().add(btnLaunch);
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch"));
|
||||
btnLaunch.setOnAction(event -> {
|
||||
World world = getItem();
|
||||
if (world != null)
|
||||
@@ -359,12 +368,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
if (supportQuickPlay) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
launchItem.setDisable(worldLocked);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
launchItem,
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> page.generateLaunchScript(world), popup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
+9
-8
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
@@ -24,6 +24,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -54,7 +55,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final HMCLGameRepository repository;
|
||||
private final String instanceId;
|
||||
private final GameInstanceID instanceId;
|
||||
private final boolean supportQuickPlay;
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
@@ -69,7 +70,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
|
||||
|
||||
public WorldManagePage(World world, HMCLGameRepository repository, String instanceId) {
|
||||
public WorldManagePage(World world, HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.world = world;
|
||||
this.backupsDir = repository.getBackupsDirectory(instanceId);
|
||||
this.repository = repository;
|
||||
@@ -150,11 +151,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,7 +233,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
{
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||
toolbar.addNavigationDrawerItem(i18n("instance.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(getSkinnable().world)) {
|
||||
@@ -265,8 +266,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch"), () -> getSkinnable().launch(), managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
* 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.versions;
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
@@ -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 = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
Path file = Controllers.showSaveDialog(fileChooser);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user