Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a5b2a99d | ||
|
|
eac2740ea8 | ||
|
|
a06da53ec9 | ||
|
|
765ea24d22 | ||
|
|
bd24ab37f5 | ||
|
|
c689272c42 | ||
|
|
952cc5dd37 | ||
|
|
356df5d630 | ||
|
|
0e433c5f4b | ||
|
|
a28509de85 | ||
|
|
e45601ecda | ||
|
|
5bbf5cbfc8 | ||
|
|
afb98c20ce | ||
|
|
6b2bc4606b | ||
|
|
6b57cec576 | ||
|
|
83111b83f2 | ||
|
|
420e337c8c | ||
|
|
028e43c959 | ||
|
|
ef01acca6d | ||
|
|
110d5f5630 | ||
|
|
94497a073d | ||
|
|
ff540a95ee | ||
|
|
ff6a32cdc3 | ||
|
|
969644635a | ||
|
|
a053736e95 | ||
|
|
879f82c462 | ||
|
|
e2d8d9f93f | ||
|
|
da4ed04697 | ||
|
|
5d44df6b40 | ||
|
|
114503e0bf | ||
|
|
8bd88aa8d8 | ||
|
|
cc7d1e1aae | ||
|
|
56d4effda0 | ||
|
|
7c2dbe3dae | ||
|
|
99469077d2 | ||
|
|
ec930843da | ||
|
|
969d314802 | ||
|
|
3b11541b0a | ||
|
|
c58d5a3a81 | ||
|
|
cf9b4aacb2 | ||
|
|
e430db0cd6 | ||
|
|
1b31e96629 | ||
|
|
5c19239ddd | ||
|
|
c181c7825b | ||
|
|
80d2fefa1f | ||
|
|
8701030934 | ||
|
|
ae4c152a8b | ||
|
|
e436163be7 | ||
|
|
9b8e65449b | ||
|
|
fbb52f4e8a | ||
|
|
704eb32048 | ||
|
|
730d9b4ce8 | ||
|
|
c7b316ecba | ||
|
|
6df5a02c3f | ||
|
|
1b9fde3d60 | ||
|
|
f435cda6a3 | ||
|
|
4a69fde6e6 | ||
|
|
294a0cd7da | ||
|
|
e057e20b3c | ||
|
|
8c0ca7c731 | ||
|
|
0d591a5429 | ||
|
|
900d7595d5 | ||
|
|
5e18c0aad4 | ||
|
|
f51f982f80 | ||
|
|
7c5430424f | ||
|
|
5909c73c48 | ||
|
|
3166c1d405 | ||
|
|
27c73b4434 | ||
|
|
705f978c43 | ||
|
|
67432bf614 | ||
|
|
378c06656d | ||
|
|
e2d6904269 | ||
|
|
8ef2f8be1b | ||
|
|
584f0f39bc | ||
|
|
6529a3bb03 | ||
|
|
0e6f43ccba | ||
|
|
94b10a8b29 | ||
|
|
dc60af6d96 | ||
|
|
272c5d1b57 | ||
|
|
5bfe945fd0 | ||
|
|
3e8a295cf2 | ||
|
|
0d717b84c4 | ||
|
|
13672a57ff | ||
|
|
b19151a811 | ||
|
|
c7d9bc696a | ||
|
|
f426ea48fa | ||
|
|
8b9eeb68f7 | ||
|
|
413069d334 | ||
|
|
bab81da466 | ||
|
|
1138d063d2 | ||
|
|
041eedb765 | ||
|
|
48046cd416 | ||
|
|
9372c0903c | ||
|
|
ee79c50dea |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: QQ 群
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -31,23 +31,20 @@ jobs:
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.jar
|
||||
HMCL/build/libs/HMCL-*.jar.sha256
|
||||
path: HMCL/build/libs/HMCL-*.jar
|
||||
archive: false
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.exe
|
||||
HMCL/build/libs/HMCL-*.exe.sha256
|
||||
path: HMCL/build/libs/HMCL-*.exe
|
||||
archive: false
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
@@ -57,6 +57,7 @@ dependencies {
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.fxsvgimage)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
implementation(libs.nayuki.qrcodegen)
|
||||
@@ -66,6 +67,7 @@ dependencies {
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
embedResources(libs.lwjgl.unsafe.agent)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -155,6 +157,7 @@ val hmclProperties = buildList {
|
||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
|
||||
@@ -23,21 +23,13 @@ import com.jfoenix.controls.events.JFXDialogEvent;
|
||||
import com.jfoenix.converters.DialogTransitionConverter;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -49,7 +41,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -155,7 +146,6 @@ public class JFXDialog extends StackPane {
|
||||
});
|
||||
|
||||
contentHolder = new StackPane();
|
||||
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
|
||||
JFXDepthManager.setDepth(contentHolder, 4);
|
||||
contentHolder.setPickOnBounds(false);
|
||||
// ensure stackpane is never resized beyond it's preferred size
|
||||
@@ -381,6 +371,14 @@ public class JFXDialog extends StackPane {
|
||||
setCycleDuration(Duration.seconds(0.4));
|
||||
setDelay(Duration.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void starting() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopping() {
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXProgressBarSkin;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
@@ -51,26 +49,18 @@ public class JFXProgressBar extends ProgressBar {
|
||||
return new JFXProgressBarSkin(this);
|
||||
}
|
||||
|
||||
private boolean smoothProgress = true;
|
||||
|
||||
public boolean isSmoothProgress() {
|
||||
return smoothProgress;
|
||||
}
|
||||
|
||||
public void setSmoothProgress(boolean smoothProgress) {
|
||||
this.smoothProgress = smoothProgress;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setPrefWidth(200);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
|
||||
private DoubleProperty secondaryProgress;
|
||||
|
||||
public DoubleProperty secondaryProgressProperty() {
|
||||
if (secondaryProgress == null) {
|
||||
secondaryProgress = new SimpleDoubleProperty(this, "secondaryProgress", INDETERMINATE_PROGRESS);
|
||||
}
|
||||
return secondaryProgress;
|
||||
}
|
||||
|
||||
public double getSecondaryProgress() {
|
||||
return secondaryProgress == null ? INDETERMINATE_PROGRESS : secondaryProgress.get();
|
||||
}
|
||||
|
||||
public void setSecondaryProgress(double secondaryProgress) {
|
||||
secondaryProgressProperty().set(secondaryProgress);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.control.Skin;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/// JFXSpinner is the material design implementation of a loading spinner.
|
||||
///
|
||||
@@ -71,10 +72,10 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
|
||||
|
||||
private static final double DEFAULT_RADIUS = 12.0;
|
||||
private static final double DEFAULT_RADIUS = 16.0;
|
||||
|
||||
/**
|
||||
* specifies the radius of the spinner node, by default it's set to `12.0`
|
||||
* specifies the radius of the spinner node, by default it's set to `16.0`
|
||||
*/
|
||||
private StyleableDoubleProperty radius;
|
||||
|
||||
@@ -96,25 +97,8 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
this.radiusProperty().set(radius);
|
||||
}
|
||||
|
||||
/// specifies from which angle the spinner should start spinning
|
||||
private StyleableDoubleProperty startingAngle;
|
||||
|
||||
public final StyleableDoubleProperty startingAngleProperty() {
|
||||
if (this.startingAngle == null) {
|
||||
startingAngle = new SimpleStyleableDoubleProperty(StyleableProperties.STARTING_ANGLE,
|
||||
JFXSpinner.this,
|
||||
"startingAngle",
|
||||
0.0);
|
||||
}
|
||||
return this.startingAngle;
|
||||
}
|
||||
|
||||
public final double getStartingAngle() {
|
||||
return startingAngle != null ? startingAngle.get() : 0.0;
|
||||
}
|
||||
|
||||
public final void setStartingAngle(final double startingAngle) {
|
||||
this.startingAngleProperty().set(startingAngle);
|
||||
public double getStartingAngle() {
|
||||
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
@@ -132,26 +116,12 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXSpinner, Number> STARTING_ANGLE =
|
||||
new CssMetaData<>("-jfx-starting-angle",
|
||||
SizeConverter.getInstance(), 0.0) {
|
||||
@Override
|
||||
public boolean isSettable(JFXSpinner control) {
|
||||
return control.startingAngle == null || !control.startingAngle.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
|
||||
return control.startingAngleProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
|
||||
Collections.addAll(styleables, RADIUS, STARTING_ANGLE);
|
||||
Collections.addAll(styleables, RADIUS);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ public class JFXTextField extends TextField {
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
|
||||
@@ -42,11 +42,11 @@ public final class JFXDepthManager {
|
||||
|
||||
private static final DropShadow[] depth = new DropShadow[] {
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 1),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 6, 0, 0, 2),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 10, 0, 0, 3),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 14, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 6)
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 0),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 8, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 12, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 16, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 8)
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,12 +40,16 @@ import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
*/
|
||||
@@ -78,7 +82,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
|
||||
// create JFX Decorator
|
||||
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
|
||||
pickerDecorator.setOnCloseButtonAction(this::updateColor);
|
||||
pickerDecorator.setOnCloseButtonAction(this::close);
|
||||
pickerDecorator.setPickOnBounds(false);
|
||||
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
|
||||
StyleSheets.init(customScene);
|
||||
@@ -153,6 +157,22 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
paraTransition.play();
|
||||
});
|
||||
|
||||
container.getChildren().add(tabs);
|
||||
|
||||
HBox actionsHBox = new HBox();
|
||||
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(event -> updateColor());
|
||||
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(event -> close());
|
||||
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
|
||||
|
||||
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
|
||||
container.getChildren().add(actionsHBox);
|
||||
|
||||
initRun = () -> {
|
||||
// change tabs labels font color according to the selected color
|
||||
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
|
||||
@@ -179,7 +199,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
String rgb = String.format("rgba(%d, %d, %d, 1)",
|
||||
String rgb = String.format("rgb(%d, %d, %d)",
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
@@ -232,7 +252,13 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return pane.getBackground().getFills().get(0).getFill();
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
|
||||
return ColorScheme.newBuilder(Themes.getColorScheme())
|
||||
.setPrimaryColorSeed(fill)
|
||||
.build()
|
||||
.getPrimary();
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(() -> {
|
||||
@@ -262,9 +288,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
container.getChildren().add(tabs);
|
||||
|
||||
this.getChildren().add(container);
|
||||
this.setPadding(new Insets(0));
|
||||
|
||||
@@ -294,7 +317,11 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
if (!systemChange) {
|
||||
userChange = true;
|
||||
try {
|
||||
curvedColorPicker.setColor(Color.valueOf(colorWebString));
|
||||
Color color = Color.valueOf(colorWebString);
|
||||
if (color.getOpacity() != 1.0) {
|
||||
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
|
||||
}
|
||||
curvedColorPicker.setColor(color);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// if color is not valid then do nothing
|
||||
}
|
||||
@@ -309,6 +336,9 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
|
||||
public void setCurrentColor(Color currentColor) {
|
||||
this.currentColorProperty.set(currentColor);
|
||||
if (curvedColorPicker != null && currentColor != null) {
|
||||
curvedColorPicker.setColor(currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
Color getCurrentColor() {
|
||||
|
||||
@@ -20,61 +20,38 @@
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.skin.ProgressIndicatorSkin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
/// # Material Design ProgressBar Skin
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-10-06
|
||||
public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
|
||||
|
||||
private StackPane track;
|
||||
private StackPane secondaryBar;
|
||||
private StackPane bar;
|
||||
private double barWidth = 0;
|
||||
private double secondaryBarWidth = 0;
|
||||
private Timeline indeterminateTransition;
|
||||
private Region clip;
|
||||
private static final double DEFAULT_HEIGHT = 4;
|
||||
|
||||
private final StackPane track;
|
||||
private final StackPane bar;
|
||||
private final Rectangle clip;
|
||||
private Animation transition;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
private double fullWidth;
|
||||
|
||||
public JFXProgressBarSkin(JFXProgressBar bar) {
|
||||
super(bar);
|
||||
public JFXProgressBarSkin(JFXProgressBar control) {
|
||||
super(control);
|
||||
|
||||
this.treeShowingProperty = new TreeShowingProperty(bar);
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
|
||||
bar.widthProperty().addListener(observable -> {
|
||||
updateProgress();
|
||||
updateSecondaryProgress();
|
||||
});
|
||||
|
||||
registerChangeListener(bar.progressProperty(), (obs) -> updateProgress());
|
||||
registerChangeListener(bar.secondaryProgressProperty(), obs -> updateSecondaryProgress());
|
||||
registerChangeListener(bar.visibleProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(bar.parentProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(bar.sceneProperty(), obs -> updateAnimation());
|
||||
|
||||
unregisterChangeListeners(treeShowingProperty);
|
||||
unregisterChangeListeners(bar.indeterminateProperty());
|
||||
|
||||
registerChangeListener(treeShowingProperty, obs -> this.updateAnimation());
|
||||
registerChangeListener(bar.indeterminateProperty(), obs -> initialize());
|
||||
|
||||
initialize();
|
||||
|
||||
getSkinnable().requestLayout();
|
||||
}
|
||||
|
||||
protected void initialize() {
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
|
||||
|
||||
track = new StackPane();
|
||||
track.getStyleClass().setAll("track");
|
||||
@@ -82,14 +59,14 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
bar = new StackPane();
|
||||
bar.getStyleClass().setAll("bar");
|
||||
|
||||
secondaryBar = new StackPane();
|
||||
secondaryBar.getStyleClass().setAll("secondary-bar");
|
||||
clip = new Rectangle();
|
||||
clip.setArcWidth(DEFAULT_HEIGHT);
|
||||
clip.setArcHeight(DEFAULT_HEIGHT);
|
||||
bar.setClip(clip);
|
||||
|
||||
clip = new Region();
|
||||
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
bar.backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(bar.getBackground(), clip));
|
||||
getChildren().setAll(track, bar);
|
||||
|
||||
getChildren().setAll(track, secondaryBar, bar);
|
||||
getSkinnable().requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,7 +81,7 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return topInset + bar.prefHeight(width) + bottomInset;
|
||||
return topInset + DEFAULT_HEIGHT + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,105 +97,129 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
track.resizeRelocate(x, y, w, h);
|
||||
secondaryBar.resizeRelocate(x, y, secondaryBarWidth, h);
|
||||
bar.resizeRelocate(x, y, getSkinnable().isIndeterminate() ? w : barWidth, h);
|
||||
clip.resizeRelocate(0, 0, w, h);
|
||||
bar.resizeRelocate(x, y, w, h);
|
||||
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
createIndeterminateTimeline();
|
||||
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
|
||||
indeterminateTransition.play();
|
||||
}
|
||||
// apply clip
|
||||
bar.setClip(clip);
|
||||
} else if (indeterminateTransition != null) {
|
||||
clip.relocate(0, 0);
|
||||
clip.setWidth(0);
|
||||
clip.setHeight(h);
|
||||
clip.setTranslateX(0);
|
||||
|
||||
fullWidth = w;
|
||||
|
||||
clearAnimation();
|
||||
updateProgress(false);
|
||||
}
|
||||
|
||||
private boolean wasIndeterminate = false;
|
||||
|
||||
private void updateProgress(boolean playProgressAnimation) {
|
||||
double progress = Math.min(getSkinnable().getProgress(), 1.0);
|
||||
boolean isIndeterminate = progress < 0.0;
|
||||
boolean isTreeShowing = treeShowingProperty.get();
|
||||
|
||||
if (isIndeterminate != wasIndeterminate) {
|
||||
wasIndeterminate = isIndeterminate;
|
||||
clearAnimation();
|
||||
// remove clip
|
||||
bar.setClip(null);
|
||||
clip.setTranslateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateSecondaryProgress() {
|
||||
final JFXProgressBar control = (JFXProgressBar) getSkinnable();
|
||||
secondaryBarWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
|
||||
* Math.min(1, Math.max(0, control.getSecondaryProgress()))) / 2.0F;
|
||||
control.requestLayout();
|
||||
}
|
||||
|
||||
boolean wasIndeterminate = false;
|
||||
|
||||
protected void pauseTimeline(boolean pause) {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (indeterminateTransition == null) {
|
||||
createIndeterminateTimeline();
|
||||
if (isIndeterminate) { // indeterminate
|
||||
if (isTreeShowing) {
|
||||
if (transition == null) {
|
||||
transition = createIndeterminateTransition();
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
transition.play();
|
||||
}
|
||||
} else if (transition != null) {
|
||||
transition.pause();
|
||||
}
|
||||
if (pause) {
|
||||
indeterminateTransition.pause();
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
if (isTreeShowing && playProgressAnimation
|
||||
&& AnimationUtils.isAnimationEnabled()
|
||||
&& getSkinnable().isSmoothProgress()) {
|
||||
transition = createDeterminateTransition(progress);
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
indeterminateTransition.play();
|
||||
clip.setWidth(computeBarWidth(progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAnimation() {
|
||||
final boolean isTreeShowing = treeShowingProperty.get();
|
||||
if (indeterminateTransition != null) {
|
||||
pauseTimeline(!isTreeShowing);
|
||||
} else if (isTreeShowing) {
|
||||
createIndeterminateTimeline();
|
||||
}
|
||||
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
|
||||
|
||||
private Transition createIndeterminateTransition() {
|
||||
double minWidth = 0;
|
||||
double maxWidth = fullWidth * 0.4;
|
||||
Transition transition = new Transition() {
|
||||
{
|
||||
setInterpolator(Interpolator.LINEAR);
|
||||
setCycleDuration(INDETERMINATE_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
double currentWidth;
|
||||
|
||||
if (frac <= 0.5) {
|
||||
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
|
||||
} else {
|
||||
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
|
||||
}
|
||||
|
||||
double targetCenter;
|
||||
if (frac <= 0.1) {
|
||||
targetCenter = 0.0;
|
||||
} else if (frac >= 0.9) {
|
||||
targetCenter = fullWidth;
|
||||
} else {
|
||||
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
|
||||
}
|
||||
|
||||
clip.setWidth(currentWidth);
|
||||
clip.setTranslateX(targetCenter - currentWidth / 2.0);
|
||||
}
|
||||
};
|
||||
|
||||
transition.setCycleCount(Timeline.INDEFINITE);
|
||||
return transition;
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
final ProgressIndicator control = getSkinnable();
|
||||
final boolean isIndeterminate = control.isIndeterminate();
|
||||
if (!(isIndeterminate && wasIndeterminate)) {
|
||||
barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
|
||||
* Math.min(1, Math.max(0, control.getProgress()))) / 2.0F;
|
||||
control.requestLayout();
|
||||
}
|
||||
wasIndeterminate = isIndeterminate;
|
||||
}
|
||||
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
|
||||
|
||||
private void createIndeterminateTimeline() {
|
||||
if (indeterminateTransition != null) {
|
||||
clearAnimation();
|
||||
}
|
||||
double dur = 1;
|
||||
ProgressIndicator control = getSkinnable();
|
||||
final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
|
||||
indeterminateTransition = new Timeline(new KeyFrame(
|
||||
Duration.ZERO,
|
||||
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_IN),
|
||||
new KeyValue(clip.translateXProperty(), -w / 2, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(0.5 * dur),
|
||||
new KeyValue(clip.scaleXProperty(), 0.4, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(0.9 * dur),
|
||||
new KeyValue(clip.translateXProperty(), w / 2, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(1 * dur),
|
||||
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_OUT)
|
||||
));
|
||||
indeterminateTransition.setCycleCount(Timeline.INDEFINITE);
|
||||
private Timeline createDeterminateTransition(double targetProgress) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
|
||||
new KeyFrame(DETERMINATE_DURATION,
|
||||
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
if (transition == timeline) {
|
||||
transition = null;
|
||||
}
|
||||
});
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
indeterminateTransition.stop();
|
||||
indeterminateTransition.getKeyFrames().clear();
|
||||
indeterminateTransition = null;
|
||||
if (transition != null) {
|
||||
transition.stop();
|
||||
transition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
treeShowingProperty.dispose();
|
||||
if (indeterminateTransition != null) {
|
||||
clearAnimation();
|
||||
}
|
||||
clearAnimation();
|
||||
}
|
||||
|
||||
private double computeBarWidth(double progress) {
|
||||
assert progress >= 0 && progress <= 1;
|
||||
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
|
||||
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
|
||||
}
|
||||
}
|
||||
@@ -20,25 +20,24 @@
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Arc;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXSpinner material design skin
|
||||
///
|
||||
@@ -47,10 +46,7 @@ import javafx.util.Duration;
|
||||
/// @since 2017-09-25
|
||||
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
|
||||
private static final Color GREEN_COLOR = Color.valueOf("#0F9D58");
|
||||
private static final Color RED_COLOR = Color.valueOf("#db4437");
|
||||
private static final Color YELLOW_COLOR = Color.valueOf("#f4b400");
|
||||
private static final Color BLUE_COLOR = Color.valueOf("#4285f4");
|
||||
private static final double DEFAULT_STROKE_WIDTH = 4;
|
||||
|
||||
private JFXSpinner control;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
@@ -61,110 +57,62 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
private Arc track;
|
||||
private final StackPane arcPane;
|
||||
private final Rectangle fillRect;
|
||||
private double arcLength = -1;
|
||||
private final Text text;
|
||||
|
||||
private final double startingAngle;
|
||||
|
||||
public JFXSpinnerSkin(JFXSpinner control) {
|
||||
super(control);
|
||||
|
||||
this.control = control;
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
this.startingAngle = control.getStartingAngle();
|
||||
|
||||
arc = new Arc();
|
||||
arc.setManaged(false);
|
||||
arc.setStartAngle(0);
|
||||
arc.setLength(180);
|
||||
arc.getStyleClass().setAll("arc");
|
||||
arc.setFill(Color.TRANSPARENT);
|
||||
arc.setStrokeWidth(3);
|
||||
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
arc.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
|
||||
track = new Arc();
|
||||
track.setManaged(false);
|
||||
track.setStartAngle(0);
|
||||
track.setLength(360);
|
||||
track.setStrokeWidth(3);
|
||||
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
track.getStyleClass().setAll("track");
|
||||
track.setFill(Color.TRANSPARENT);
|
||||
|
||||
fillRect = new Rectangle();
|
||||
fillRect.setFill(Color.TRANSPARENT);
|
||||
text = new Text();
|
||||
text.setStyle("-fx-font-size:null");
|
||||
text.getStyleClass().setAll("text", "percentage");
|
||||
final Group group = new Group(fillRect, track, arc, text);
|
||||
final Group group = new Group(fillRect, track, arc);
|
||||
group.setManaged(false);
|
||||
arcPane = new StackPane(group);
|
||||
arcPane.setPrefSize(50, 50);
|
||||
getChildren().setAll(arcPane);
|
||||
|
||||
// register listeners
|
||||
registerChangeListener(control.indeterminateProperty(), obs -> initialize());
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress());
|
||||
registerChangeListener(treeShowingProperty, obs -> updateAnimation());
|
||||
registerChangeListener(control.sceneProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress());
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (timeline == null) {
|
||||
createTransition();
|
||||
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
|
||||
private void updateProgress() {
|
||||
double progress = Double.min(getSkinnable().getProgress(), 1.0);
|
||||
if (progress < 0) { // indeterminate
|
||||
boolean treeShowing = treeShowingProperty.get();
|
||||
if (treeShowing) {
|
||||
if (timeline == null) {
|
||||
timeline = createTransition();
|
||||
timeline.playFromStart();
|
||||
} else {
|
||||
timeline.play();
|
||||
}
|
||||
} else if (timeline != null) {
|
||||
timeline.pause();
|
||||
}
|
||||
} else {
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
arc.setStartAngle(90);
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyFrame[] getKeyFrames(double angle, double duration, Paint color) {
|
||||
KeyFrame[] frames = new KeyFrame[4];
|
||||
frames[0] = new KeyFrame(Duration.seconds(duration),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 45 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[1] = new KeyFrame(Duration.seconds(duration + 0.4),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 90 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[2] = new KeyFrame(Duration.seconds(duration + 0.7),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 135 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[3] = new KeyFrame(Duration.seconds(duration + 1.1),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 435 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR),
|
||||
new KeyValue(arc.strokeProperty(), color, Interpolator.EASE_BOTH));
|
||||
return frames;
|
||||
}
|
||||
|
||||
private void pauseTimeline(boolean pause) {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (timeline == null) {
|
||||
createTransition();
|
||||
}
|
||||
if (pause) {
|
||||
timeline.pause();
|
||||
} else {
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAnimation() {
|
||||
final boolean isTreeShowing = treeShowingProperty.get();
|
||||
if (timeline != null) {
|
||||
pauseTimeline(!isTreeShowing);
|
||||
} else if (isTreeShowing) {
|
||||
createTransition();
|
||||
arc.setLength(-360 * progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,21 +172,9 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
fillRect.setHeight(arcSize);
|
||||
|
||||
if (!isValid) {
|
||||
initialize();
|
||||
updateProgress();
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (!getSkinnable().isIndeterminate()) {
|
||||
arc.setLength(arcLength);
|
||||
if (text.isVisible()) {
|
||||
final double progress = control.getProgress();
|
||||
int intProgress = (int) Math.round(progress * 100.0);
|
||||
Font font = text.getFont();
|
||||
text.setFont(Font.font(font.getFamily(), radius / 1.7));
|
||||
text.setText((progress > 1 ? 100 : intProgress) + "%");
|
||||
text.relocate((arcSize - text.getLayoutBounds().getWidth()) / 2, (arcSize - text.getLayoutBounds().getHeight()) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateArcLayout(double radius, double arcSize) {
|
||||
@@ -254,66 +190,64 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
track.setStrokeWidth(arc.getStrokeWidth());
|
||||
}
|
||||
|
||||
boolean wasIndeterminate = false;
|
||||
|
||||
protected void updateProgress() {
|
||||
final ProgressIndicator control = getSkinnable();
|
||||
final boolean isIndeterminate = control.isIndeterminate();
|
||||
if (!(isIndeterminate && wasIndeterminate)) {
|
||||
arcLength = -360 * control.getProgress();
|
||||
control.requestLayout();
|
||||
}
|
||||
wasIndeterminate = isIndeterminate;
|
||||
}
|
||||
|
||||
private void createTransition() {
|
||||
if (!getSkinnable().isIndeterminate()) return;
|
||||
final Paint initialColor = arc.getStroke();
|
||||
if (initialColor == null) {
|
||||
arc.setStroke(BLUE_COLOR);
|
||||
}
|
||||
|
||||
KeyFrame[] blueFrame = getKeyFrames(0, 0, initialColor == null ? BLUE_COLOR : initialColor);
|
||||
KeyFrame[] redFrame = getKeyFrames(450, 1.4, initialColor == null ? RED_COLOR : initialColor);
|
||||
KeyFrame[] yellowFrame = getKeyFrames(900, 2.8, initialColor == null ? YELLOW_COLOR : initialColor);
|
||||
KeyFrame[] greenFrame = getKeyFrames(1350, 4.2, initialColor == null ? GREEN_COLOR : initialColor);
|
||||
|
||||
KeyFrame endingFrame = new KeyFrame(Duration.seconds(5.6),
|
||||
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
||||
frames.add(new KeyFrame(Duration.seconds(duration),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
1845 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
angle + 45 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 90 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 135 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 435 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
}
|
||||
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
private Timeline createTransition() {
|
||||
Timeline timeline;
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
var keyFrames = new ArrayList<KeyFrame>(17);
|
||||
addKeyFrames(keyFrames, 0, 0);
|
||||
addKeyFrames(keyFrames, 450, 1.4);
|
||||
addKeyFrames(keyFrames, 900, 2.8);
|
||||
addKeyFrames(keyFrames, 1350, 4.2);
|
||||
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
1845 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
|
||||
timeline = new Timeline();
|
||||
timeline.getKeyFrames().setAll(keyFrames);
|
||||
} else {
|
||||
final double arcLength = 250;
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
||||
new KeyFrame(Duration.seconds(1.2),
|
||||
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
||||
);
|
||||
}
|
||||
timeline = new Timeline(blueFrame[0],
|
||||
blueFrame[1],
|
||||
blueFrame[2],
|
||||
blueFrame[3],
|
||||
redFrame[0],
|
||||
redFrame[1],
|
||||
redFrame[2],
|
||||
redFrame[3],
|
||||
yellowFrame[0],
|
||||
yellowFrame[1],
|
||||
yellowFrame[2],
|
||||
yellowFrame[3],
|
||||
greenFrame[0],
|
||||
greenFrame[1],
|
||||
greenFrame[2],
|
||||
greenFrame[3],
|
||||
endingFrame);
|
||||
|
||||
timeline.setCycleCount(Timeline.INDEFINITE);
|
||||
timeline.setDelay(Duration.ZERO);
|
||||
timeline.playFromStart();
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
timeline = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
@@ -49,6 +50,8 @@ public final class EntryPoint {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkWine();
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
@@ -220,6 +223,20 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkWine() {
|
||||
if (OperatingSystem.isRunningUnderWine()) {
|
||||
SwingUtils.initLookAndFeel();
|
||||
LOG.warning("HMCL is running under Wine or its distributions!");
|
||||
|
||||
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
|
||||
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
|
||||
@@ -17,35 +17,36 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -65,8 +66,8 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||
@@ -111,7 +112,7 @@ public final class Launcher extends Application {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& ConfigHolder.isNewlyCreated()
|
||||
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
|
||||
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
|
||||
return;
|
||||
} else {
|
||||
checkConfigInTempDir();
|
||||
@@ -182,6 +183,27 @@ public final class Launcher extends Application {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
|
||||
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
|
||||
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
|
||||
|
||||
okButton.setDisable(true);
|
||||
|
||||
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
|
||||
for (int i = 0; i < seconds; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
||||
}
|
||||
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
|
||||
new KeyValue(okButton.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(okButton.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
alert.setOnShown(e -> timeline.play());
|
||||
alert.setOnCloseRequest(e -> timeline.stop());
|
||||
return alert.showAndWait().orElse(null) == ButtonType.YES;
|
||||
}
|
||||
|
||||
private static boolean isConfigInTempDir() {
|
||||
String configPath = ConfigHolder.configLocation().toString();
|
||||
|
||||
@@ -221,7 +243,7 @@ public final class Launcher extends Application {
|
||||
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
|
||||
EntryPoint.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public final class Metadata {
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
|
||||
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
|
||||
|
||||
public static final String DOCS_URL = "https://docs.hmcl.net";
|
||||
public static final String CONTACT_URL = DOCS_URL + "/help.html";
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.countly;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
@@ -21,12 +21,16 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
@@ -150,4 +154,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
generateOptionsTxt();
|
||||
super.makeLaunchScript(scriptFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendJvmArgs(CommandBuilder result) {
|
||||
super.appendJvmArgs(result);
|
||||
|
||||
if (config().getAllowAutoAgent()
|
||||
&& !options.isNoGeneratedJVMArgs()
|
||||
&& !options.isNoGeneratedOptimizingJVMArgs()
|
||||
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
|
||||
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
|
||||
try {
|
||||
result.add("-javaagent:" + extractLwjglUnsafeAgent());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Path extractLwjglUnsafeAgent() throws IOException {
|
||||
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
|
||||
if (agentVersion == null) {
|
||||
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
|
||||
}
|
||||
|
||||
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
|
||||
String fileName = library.getArtifact().getFileName();
|
||||
|
||||
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
|
||||
if (agentPath.toString().contains("=")) {
|
||||
throw new IOException("Invalid library path: " + agentPath);
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
|
||||
if (input == null) {
|
||||
throw new IOException("/assets/" + fileName + " not found");
|
||||
}
|
||||
|
||||
bytes = input.readAllBytes();
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(agentPath)) {
|
||||
try {
|
||||
if (Files.size(agentPath) == bytes.length) {
|
||||
return agentPath;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check size of " + agentPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
Files.createDirectories(agentPath.getParent());
|
||||
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
|
||||
return agentPath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
Optional<Path> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile.get());
|
||||
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static javafx.application.Platform.runLater;
|
||||
import static javafx.application.Platform.setImplicitExit;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
@@ -75,6 +76,8 @@ import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
|
||||
|
||||
public final class LauncherHelper {
|
||||
|
||||
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
|
||||
|
||||
private final Profile profile;
|
||||
private Account account;
|
||||
private final String selectedVersion;
|
||||
@@ -197,6 +200,28 @@ public final class LauncherHelper {
|
||||
);
|
||||
}).withStage("launch.state.dependencies")
|
||||
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
|
||||
.thenComposeAsync(() -> {
|
||||
if (config().getAllowAutoAgent()
|
||||
|| setting.isNoJVMArgs()
|
||||
|| setting.isNoOptimizingJVMArgs()
|
||||
|| Boolean.TRUE.equals(config().getShownTips().get(LWJGL_3_4_1_TIP))
|
||||
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
|
||||
return Task.completed(null);
|
||||
} else {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
runInFX(() -> {
|
||||
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
|
||||
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
|
||||
config().setAllowAutoAgent(true);
|
||||
future.complete(null);
|
||||
}, () -> {
|
||||
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
|
||||
future.complete(null);
|
||||
});
|
||||
});
|
||||
return Task.fromCompletableFuture(future);
|
||||
}
|
||||
})
|
||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
|
||||
@@ -387,7 +412,16 @@ public final class LauncherHelper {
|
||||
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
|
||||
try {
|
||||
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
|
||||
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
GameJavaVersion minimumJavaVersion = null;
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (minimumJavaVersion == null)
|
||||
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
|
||||
Controllers.dialog(
|
||||
@@ -402,8 +436,17 @@ public final class LauncherHelper {
|
||||
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} else
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
} else {
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJavaVersion == null)
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||
downloadJava(targetJavaVersion, profile)
|
||||
@@ -433,7 +476,7 @@ 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)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violatedMandatoryConstraints.add(constraint);
|
||||
} else {
|
||||
@@ -468,9 +511,14 @@ public final class LauncherHelper {
|
||||
return result;
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
|
||||
.orElse("");
|
||||
|
||||
gameJavaVersion = !cleanroomVersion.isEmpty()
|
||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
||||
: GameJavaVersion.JAVA_21;
|
||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
@@ -560,9 +608,14 @@ public final class LauncherHelper {
|
||||
case MODDED_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||
break;
|
||||
case CLEANROOM_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.cleanroom"));
|
||||
case CLEANROOM: {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
|
||||
if (!cleanroomVersion.isEmpty())
|
||||
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
|
||||
break;
|
||||
}
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
|
||||
@@ -115,8 +115,8 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(id);
|
||||
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +125,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(id);
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ public final class JavaManager {
|
||||
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violationMandatory = true;
|
||||
} else {
|
||||
|
||||
@@ -222,6 +222,36 @@ public final class Config extends ObservableSetting {
|
||||
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
|
||||
}
|
||||
|
||||
@SerializedName("disableAutoShowUpdateDialog")
|
||||
private final BooleanProperty disableAutoShowUpdateDialog = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty disableAutoShowUpdateDialogProperty() {
|
||||
return disableAutoShowUpdateDialog;
|
||||
}
|
||||
|
||||
public boolean isDisableAutoShowUpdateDialog() {
|
||||
return disableAutoShowUpdateDialog.get();
|
||||
}
|
||||
|
||||
public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog) {
|
||||
this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog);
|
||||
}
|
||||
|
||||
@SerializedName("disableAprilFools")
|
||||
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty disableAprilFoolsProperty() {
|
||||
return disableAprilFools;
|
||||
}
|
||||
|
||||
public boolean isDisableAprilFools() {
|
||||
return disableAprilFools.get();
|
||||
}
|
||||
|
||||
public void setDisableAprilFools(boolean disableAprilFools) {
|
||||
this.disableAprilFools.set(disableAprilFools);
|
||||
}
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
@@ -537,6 +567,21 @@ public final class Config extends ObservableSetting {
|
||||
this.versionListSource.set(versionListSource);
|
||||
}
|
||||
|
||||
@SerializedName("defaultAddonSource")
|
||||
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
|
||||
|
||||
public StringProperty defaultAddonSourceProperty() {
|
||||
return defaultAddonSource;
|
||||
}
|
||||
|
||||
public String getDefaultAddonSource() {
|
||||
return defaultAddonSource.get();
|
||||
}
|
||||
|
||||
public void setDefaultAddonSource(String defaultAddonSource) {
|
||||
this.defaultAddonSource.set(defaultAddonSource);
|
||||
}
|
||||
|
||||
@SerializedName("hasProxy")
|
||||
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
|
||||
|
||||
@@ -659,6 +704,21 @@ public final class Config extends ObservableSetting {
|
||||
this.disableAutoGameOptions.set(disableAutoGameOptions);
|
||||
}
|
||||
|
||||
@SerializedName("allowAutoAgent")
|
||||
private final BooleanProperty allowAutoAgent = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty allowAutoAgentProperty() {
|
||||
return allowAutoAgent;
|
||||
}
|
||||
|
||||
public boolean getAllowAutoAgent() {
|
||||
return allowAutoAgent.get();
|
||||
}
|
||||
|
||||
public void setAllowAutoAgent(boolean allowAutoAgent) {
|
||||
this.allowAutoAgent.set(allowAutoAgent);
|
||||
}
|
||||
|
||||
// Accounts
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
@@ -64,12 +65,16 @@ 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.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -82,6 +87,7 @@ public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
public static final String APRIL_FOOLS = "aprilFools";
|
||||
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
@@ -225,6 +231,8 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
LOG.info("April Fools: " + AprilFools.isEnabled());
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
@@ -432,6 +440,53 @@ public final class Controllers {
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
|
||||
aprilFools:
|
||||
if (AprilFools.isEnabled()) {
|
||||
int currentYear = LocalDate.now().getYear();
|
||||
if (config().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
|
||||
break aprilFools;
|
||||
|
||||
if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
|
||||
break aprilFools;
|
||||
|
||||
SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
|
||||
.filter(locale -> "lzh".equals(locale.getName()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (lzh == null) {
|
||||
LOG.warning("No supported locale found for lzh");
|
||||
break aprilFools;
|
||||
}
|
||||
|
||||
Runnable updateShowTips = () -> config().getShownTips().put(APRIL_FOOLS, currentYear);
|
||||
|
||||
Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
|
||||
MessageType.QUESTION, () -> {
|
||||
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
|
||||
LOG.info("Switching locale to " + lzh);
|
||||
|
||||
updateShowTips.run();
|
||||
config().setLocalization(lzh);
|
||||
|
||||
Controllers.onApplicationStop();
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
try {
|
||||
Restarter.restartSelf();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to restart self", e);
|
||||
}
|
||||
|
||||
Platform.exit();
|
||||
}, updateShowTips);
|
||||
}, updateShowTips);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -66,6 +67,7 @@ import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
@@ -233,8 +235,8 @@ public final class FXUtils {
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
|
||||
public static final List<String> IMAGE_EXTENSIONS = List.of(
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
|
||||
);
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
@@ -1239,8 +1241,8 @@ public final class FXUtils {
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uri)
|
||||
public static Task<Image> getRemoteImageTask(List<URI> uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uris)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
@@ -1288,6 +1290,23 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static void setOnActionWithCooldown(ButtonBase button, Runnable action) {
|
||||
setOnActionWithCooldown(button, action, Motion.SHORT4);
|
||||
}
|
||||
|
||||
public static void setOnActionWithCooldown(ButtonBase button, Runnable action, Duration cooldown) {
|
||||
button.setOnAction(e -> {
|
||||
button.setDisable(true);
|
||||
|
||||
var pause = new PauseTransition(cooldown);
|
||||
pause.setOnFinished(event -> button.setDisable(false));
|
||||
pause.play();
|
||||
|
||||
action.run();
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel() {
|
||||
Label label = new Label();
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
@@ -273,10 +274,10 @@ public class GameCrashWindow extends Stage {
|
||||
logWindow.show();
|
||||
}
|
||||
|
||||
private void exportGameCrashInfo() {
|
||||
private CompletableFuture<Path> exportGameCrashInfo() {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
|
||||
.thenComposeAsync(logs -> {
|
||||
long processStartTime = managedProcess.getProcess().info()
|
||||
@@ -301,20 +302,7 @@ public class GameCrashWindow extends Stage {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.handleAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}).thenApply(ignored -> logFile);
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
@@ -444,8 +432,35 @@ public class GameCrashWindow extends Stage {
|
||||
HBox toolBar = new HBox();
|
||||
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
|
||||
SpinnerPane exportButtonPane = new SpinnerPane();
|
||||
exportButtonPane.getStyleClass().add("small-spinner-pane");
|
||||
|
||||
JFXButton exportButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportButtonPane.setContent(exportButton);
|
||||
exportButton.setOnAction(e -> {
|
||||
exportButtonPane.showSpinner();
|
||||
exportGameCrashInfo().whenCompleteAsync((result, exception) -> {
|
||||
exportButtonPane.hideSpinner();
|
||||
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(result);
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("settings.launcher.launcher_log.export.success", result),
|
||||
i18n("message.success"),
|
||||
MessageDialogPane.MessageType.SUCCESS
|
||||
).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
i18n("message.error"),
|
||||
MessageDialogPane.MessageType.ERROR
|
||||
).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
|
||||
logButton.setOnAction(e -> showLogWindow());
|
||||
@@ -457,7 +472,7 @@ public class GameCrashWindow extends Stage {
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
|
||||
toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
|
||||
@@ -36,6 +36,7 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
@@ -241,6 +242,10 @@ public final class LogWindow extends Stage {
|
||||
});
|
||||
}
|
||||
|
||||
private ManagedProcess getGameProcess() {
|
||||
return gameProcess;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new LogWindowSkin(this);
|
||||
@@ -411,11 +416,12 @@ public final class LogWindow extends Stage {
|
||||
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
SpinnerPane exportDumpPane = new SpinnerPane();
|
||||
exportDumpPane.getStyleClass().add("small-spinner-pane");
|
||||
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
|
||||
} else {
|
||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
|
||||
FXUtils.installFastTooltip(exportDumpPane, i18n("logwindow.export_dump.no_dependency"));
|
||||
exportDumpButton.setDisable(true);
|
||||
}
|
||||
exportDumpPane.setContent(exportDumpButton);
|
||||
@@ -424,6 +430,13 @@ public final class LogWindow extends Stage {
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
control.getGameProcess().getProcess()
|
||||
.onExit()
|
||||
.thenRunAsync(() -> {
|
||||
terminateButton.setDisable(true);
|
||||
exportDumpButton.setDisable(true);
|
||||
}, Schedulers.javafx());
|
||||
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/// @author Glavo
|
||||
public final class MemoryStatusBar extends Control {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "memory-status-bar";
|
||||
|
||||
public MemoryStatusBar() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
setPrefWidth(200);
|
||||
}
|
||||
|
||||
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
|
||||
|
||||
/// The memory status of the system.
|
||||
///
|
||||
/// The property will be automatically updated.
|
||||
///
|
||||
/// The property has no bean, because it may share between multiple MemoryStatusBar instances.
|
||||
public ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
return memoryStatus;
|
||||
}
|
||||
|
||||
public PhysicalMemoryStatus getMemoryStatus() {
|
||||
return memoryStatus.get();
|
||||
}
|
||||
|
||||
private final DoubleProperty memoryAllocated = new SimpleDoubleProperty();
|
||||
|
||||
public DoubleProperty memoryAllocatedProperty() {
|
||||
return memoryAllocated;
|
||||
}
|
||||
|
||||
public double getMemoryAllocated() {
|
||||
return memoryAllocated.get();
|
||||
}
|
||||
|
||||
public void setMemoryAllocated(double memoryAllocated) {
|
||||
this.memoryAllocated.set(memoryAllocated);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SkinBase<MemoryStatusBar> createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
private static final class Skin extends SkinBase<MemoryStatusBar> {
|
||||
private static final int HEIGHT = 4;
|
||||
|
||||
private final StackPane track;
|
||||
private final Rectangle memoryUsed;
|
||||
private final Rectangle memoryAllocate;
|
||||
|
||||
private double contentWidth = -1;
|
||||
|
||||
Skin(MemoryStatusBar control) {
|
||||
super(control);
|
||||
|
||||
track = new StackPane();
|
||||
track.getStyleClass().add("track");
|
||||
track.setPadding(new Insets(HEIGHT, 0, 0, 0));
|
||||
|
||||
memoryUsed = new Rectangle();
|
||||
memoryUsed.setManaged(false);
|
||||
memoryUsed.setArcWidth(HEIGHT);
|
||||
memoryUsed.setArcHeight(HEIGHT);
|
||||
memoryUsed.getStyleClass().add("memory-used");
|
||||
|
||||
memoryAllocate = new Rectangle();
|
||||
memoryAllocate.setManaged(false);
|
||||
memoryAllocate.setArcWidth(HEIGHT);
|
||||
memoryAllocate.setArcHeight(HEIGHT);
|
||||
memoryAllocate.getStyleClass().add("memory-allocate");
|
||||
|
||||
this.getChildren().setAll(track, memoryAllocate, memoryUsed);
|
||||
|
||||
registerInvalidationListener(control.memoryStatusProperty(), it -> updateBar());
|
||||
registerInvalidationListener(control.memoryAllocatedProperty(), it -> updateBar());
|
||||
}
|
||||
|
||||
private void updateBar() {
|
||||
if (contentWidth <= 0) {
|
||||
// not yet layout
|
||||
return;
|
||||
}
|
||||
|
||||
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
|
||||
double total = status.getTotal();
|
||||
double used = status.getUsed();
|
||||
double allocated = getSkinnable().getMemoryAllocated();
|
||||
|
||||
if (total <= 0) {
|
||||
memoryUsed.setWidth(contentWidth);
|
||||
memoryAllocate.setWidth(contentWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
double usedWidth = MathUtils.clamp(contentWidth * (used / total), 0, contentWidth);
|
||||
double allocatedWidth = MathUtils.clamp(contentWidth * (allocated / total) + usedWidth, 0, contentWidth);
|
||||
|
||||
memoryUsed.setWidth(usedWidth);
|
||||
memoryAllocate.setWidth(allocatedWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
|
||||
track.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
|
||||
memoryUsed.relocate(contentX, contentY);
|
||||
memoryAllocate.relocate(contentX, contentY);
|
||||
|
||||
this.contentWidth = contentWidth;
|
||||
|
||||
memoryUsed.setHeight(contentHeight);
|
||||
memoryAllocate.setHeight(contentHeight);
|
||||
|
||||
updateBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return leftInset + track.prefWidth(height) + rightInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return topInset + HEIGHT + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UpdateMemoryStatus extends Thread {
|
||||
|
||||
private static final int UPDATE_INTERVAL = 3000;
|
||||
|
||||
@FXThread
|
||||
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
|
||||
|
||||
@FXThread
|
||||
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
if (memoryStatusPropertyCache != null) {
|
||||
var property = memoryStatusPropertyCache.get();
|
||||
if (property != null) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
memoryStatusPropertyCache = new WeakReference<>(property);
|
||||
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
|
||||
return property;
|
||||
}
|
||||
|
||||
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
|
||||
|
||||
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
|
||||
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
|
||||
|
||||
setName("UpdateMemoryStatus");
|
||||
setDaemon(true);
|
||||
setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
|
||||
|
||||
var memoryStatusProperty = memoryStatusPropertyRef.get();
|
||||
if (memoryStatusProperty == null)
|
||||
return;
|
||||
|
||||
if (Controllers.isStopped())
|
||||
return;
|
||||
|
||||
Platform.runLater(() -> memoryStatusProperty.set(status));
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(UPDATE_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
spinnerCopyUUID.setContent(btnCopyUUID);
|
||||
right.getChildren().add(spinnerCopyUUID);
|
||||
|
||||
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE);
|
||||
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
|
||||
|
||||
@@ -80,8 +80,7 @@ public class FileSelector extends HBox {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
FXUtils.bindString(customField, valueProperty());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.FOLDER_OPEN.createIcon(15));
|
||||
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
|
||||
selectButton.setOnAction(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
|
||||
// Referenced in root.css
|
||||
@SuppressWarnings("unused")
|
||||
@@ -75,7 +75,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
*/
|
||||
if (getSkinnable().getMax() > getSkinnable().getMin()) {
|
||||
dragStart = thumb.localToParent(me.getX(), me.getY());
|
||||
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
|
||||
double clampedValue = MathUtils.clamp(getSkinnable().getValue(), getSkinnable().getMin(), getSkinnable().getMax());
|
||||
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
|
||||
me.consume();
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
getSkinnable().requestFocus();
|
||||
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
|
||||
if (!Double.isNaN(newValue)) {
|
||||
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
|
||||
getSkinnable().setValue(MathUtils.clamp(newValue, getSkinnable().getMin(), getSkinnable().getMax()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +129,12 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
track.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
track.relocate(0, -5);
|
||||
track.relocate(0, -8);
|
||||
track.widthProperty().bind(scrollBar.widthProperty());
|
||||
track.setHeight(5);
|
||||
track.setHeight(8);
|
||||
} else {
|
||||
track.relocate(-5, 0);
|
||||
track.setWidth(5);
|
||||
track.relocate(-8, 0);
|
||||
track.setWidth(8);
|
||||
track.heightProperty().bind(scrollBar.heightProperty());
|
||||
}
|
||||
|
||||
@@ -144,13 +144,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
thumb.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
thumb.relocate(0, -5);
|
||||
thumb.relocate(0, -8);
|
||||
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
|
||||
thumb.setHeight(5);
|
||||
thumb.setHeight(8);
|
||||
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
|
||||
} else {
|
||||
thumb.relocate(-5, 0);
|
||||
thumb.setWidth(5);
|
||||
thumb.relocate(-8, 0);
|
||||
thumb.setWidth(8);
|
||||
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 5;
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,7 +171,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 5;
|
||||
return 8;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,9 +30,6 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
public class LineButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-button";
|
||||
|
||||
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
|
||||
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
|
||||
|
||||
public static LineButton createNavigationButton() {
|
||||
var button = new LineButton();
|
||||
button.setTrailingIcon(SVG.ARROW_FORWARD);
|
||||
@@ -50,7 +47,14 @@ public class LineButton extends LineButtonBase {
|
||||
|
||||
public LineButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
container.setMouseTransparent(true);
|
||||
}
|
||||
|
||||
protected int getTrailingTextIndex() {
|
||||
return IDX_TRAILING;
|
||||
}
|
||||
|
||||
protected int getTrailingIconIndex() {
|
||||
return getTrailingTextIndex() + 1;
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<ActionEvent>> onAction;
|
||||
@@ -110,12 +114,13 @@ public class LineButton extends LineButtonBase {
|
||||
trailingTextLabel = new Label();
|
||||
trailingTextLabel.getStyleClass().add("trailing-label");
|
||||
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
trailingTextLabel.setMouseTransparent(true);
|
||||
}
|
||||
trailingTextLabel.setText(message);
|
||||
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
|
||||
setNode(getTrailingTextIndex(), trailingTextLabel);
|
||||
} else if (trailingTextLabel != null) {
|
||||
trailingTextLabel.setText("");
|
||||
setNode(IDX_TRAILING_TEXT, null);
|
||||
setNode(getTrailingTextIndex(), null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -149,7 +154,7 @@ public class LineButton extends LineButtonBase {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_TRAILING_ICON, get());
|
||||
setNode(getTrailingIconIndex(), get());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,6 +176,7 @@ public class LineButton extends LineButtonBase {
|
||||
public void setTrailingIcon(SVG rightIcon, double size) {
|
||||
Node rightIconNode = rightIcon.createIcon(size);
|
||||
rightIconNode.getStyleClass().add("trailing-icon");
|
||||
rightIconNode.setMouseTransparent(true);
|
||||
setTrailingIcon(rightIconNode);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
|
||||
this.container = new HBox(SPACING);
|
||||
container.getStyleClass().add("line-component-container");
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
container.setPickOnBounds(true);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
titleLabel.getStyleClass().add("title-label");
|
||||
|
||||
@@ -20,16 +20,18 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
@@ -41,7 +43,7 @@ import java.util.function.Function;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineSelectButton<T> extends LineButton {
|
||||
public class LineSelectButton<T> extends LineButton {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
@@ -81,6 +83,8 @@ public final class LineSelectButton<T> extends LineButton {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
this.popup = new JFXPopup(popupMenu);
|
||||
|
||||
popupMenu.addEventHandler(KeyEvent.KEY_PRESSED, this::handleKeyEvent);
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
@@ -213,4 +217,32 @@ public final class LineSelectButton<T> extends LineButton {
|
||||
return items.get();
|
||||
}
|
||||
|
||||
private void handleKeyEvent(KeyEvent event) {
|
||||
ObservableList<T> list = getItems();
|
||||
if (list == null || list.isEmpty()) return;
|
||||
int index = list.indexOf(getValue());
|
||||
|
||||
var code = event.getCode();
|
||||
|
||||
if (code == KeyCode.UP) {
|
||||
if (index > 0) {
|
||||
setValue(list.get(index - 1));
|
||||
} else {
|
||||
setValue(list.get(list.size() - 1));
|
||||
}
|
||||
event.consume();
|
||||
} else if (code == KeyCode.DOWN) {
|
||||
if (index < list.size() - 1) {
|
||||
setValue(list.get(index + 1));
|
||||
} else {
|
||||
setValue(list.get(0));
|
||||
}
|
||||
event.consume();
|
||||
} else if (code == KeyCode.ENTER || code == KeyCode.ESCAPE) {
|
||||
if (popup != null && popup.isShowing()) {
|
||||
popup.hide();
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,7 @@ public class SpinnerPane extends Control {
|
||||
|
||||
Skin(SpinnerPane control) {
|
||||
super(control);
|
||||
root.setClip(null);
|
||||
|
||||
updateContent();
|
||||
this.getChildren().setAll(root);
|
||||
|
||||
@@ -83,8 +83,8 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 26);
|
||||
|
||||
private TaskExecutor executor;
|
||||
private final JFXListView<Node> listView = new JFXListView<>();
|
||||
@@ -279,6 +279,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
private final class Cell extends ListCell<Node> {
|
||||
private static final double STATUS_ICON_SIZE = 14;
|
||||
private static final Insets PROGRESS_BAR_MARGIN = new Insets(2, 0, 0, 0);
|
||||
|
||||
private final BorderPane pane = new BorderPane();
|
||||
private final StackPane left = new StackPane();
|
||||
@@ -310,6 +311,7 @@ public final class TaskListPane extends StackPane {
|
||||
bar.minWidthProperty().bind(barWidth);
|
||||
bar.prefWidthProperty().bind(barWidth);
|
||||
bar.maxWidthProperty().bind(barWidth);
|
||||
BorderPane.setMargin(bar, PROGRESS_BAR_MARGIN);
|
||||
|
||||
setGraphic(pane);
|
||||
}
|
||||
@@ -325,8 +327,9 @@ public final class TaskListPane extends StackPane {
|
||||
pane.paddingProperty().unbind();
|
||||
title.textProperty().unbind();
|
||||
message.textProperty().unbind();
|
||||
bar.progressProperty().unbind();
|
||||
|
||||
bar.setSmoothProgress(false);
|
||||
bar.progressProperty().unbind();
|
||||
StageNode prevStageNode;
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
@@ -364,6 +367,8 @@ public final class TaskListPane extends StackPane {
|
||||
pane.setRight(null);
|
||||
pane.setBottom(null);
|
||||
}
|
||||
|
||||
bar.setSmoothProgress(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
@@ -168,11 +169,13 @@ public class TwoLineListItem extends VBox {
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
|
||||
scrollPane.setPrefWidth(50);
|
||||
firstLine.getChildren().setAll(lblTitle, scrollPane);
|
||||
|
||||
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -50,20 +50,19 @@ 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.Lang;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -194,7 +193,10 @@ public class DecoratorController {
|
||||
String backgroundImage = config().getBackgroundImage();
|
||||
if (backgroundImage != null)
|
||||
try {
|
||||
image = tryLoadImage(Paths.get(backgroundImage));
|
||||
Path path = Path.of(backgroundImage);
|
||||
image = Files.isDirectory(path)
|
||||
? randomImageIn(path)
|
||||
: tryLoadImage(path);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Couldn't load background image", e);
|
||||
}
|
||||
@@ -216,7 +218,7 @@ public class DecoratorController {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
case PAINT:
|
||||
Paint paint = config().getBackgroundPaint();
|
||||
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
|
||||
double opacity = MathUtils.clamp(config().getBackgroundImageOpacity(), 0, 100) / 100.;
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null)
|
||||
@@ -300,14 +302,14 @@ public class DecoratorController {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Path> candidates;
|
||||
ArrayList<Path> candidates;
|
||||
try (Stream<Path> stream = Files.list(imageDir)) {
|
||||
candidates = stream
|
||||
.filter(it -> FXUtils.IMAGE_EXTENSIONS.contains(getExtension(it).toLowerCase(Locale.ROOT)))
|
||||
.filter(Files::isReadable)
|
||||
.collect(toList());
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list files in ./bg", e);
|
||||
LOG.warning("Failed to list files in " + imageDir, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
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((profile, __, mod, file) -> {
|
||||
Versions.downloadModpackImpl(profile, uploadVersion, mod, file);
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, profile, __, mod, file) -> {
|
||||
Versions.downloadModpackImpl(downloadProvider, profile, uploadVersion, mod, file);
|
||||
}, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
@@ -94,9 +94,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, mod, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, mod, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((profile, version, mod, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
@@ -129,7 +129,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
public static void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
@@ -138,7 +138,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.getFile().getUrl()), dest);
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
|
||||
@@ -124,14 +124,14 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
if (libraryType != null) {
|
||||
String loaderName = switch (libraryType) {
|
||||
case FORGE -> "forge";
|
||||
case NEO_FORGE -> "neoforge";
|
||||
case CLEANROOM -> "cleanroom";
|
||||
case LEGACY_FABRIC -> "legacy_fabric";
|
||||
case FABRIC -> "fabric";
|
||||
case LITELOADER -> "liteloader";
|
||||
case QUILT -> "quilt";
|
||||
case OPTIFINE -> "optifine";
|
||||
case FORGE -> "Forge";
|
||||
case NEO_FORGE -> "NeoForge";
|
||||
case CLEANROOM -> "Cleanroom";
|
||||
case LEGACY_FABRIC -> "LegacyFabric";
|
||||
case FABRIC -> "Fabric";
|
||||
case LITELOADER -> "LiteLoader";
|
||||
case QUILT -> "Quilt";
|
||||
case OPTIFINE -> "OptiFine";
|
||||
default -> null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
@@ -42,11 +59,11 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
{
|
||||
var archiveNamePane = new LinePane();
|
||||
{
|
||||
archiveNamePane.setTitle(i18n("archive.file.name"));
|
||||
archiveNamePane.setTitle(i18n("version.name"));
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.setPrefWidth(300);
|
||||
// FIXME: Validator are not shown properly
|
||||
FXUtils.setLimitHeight(archiveNamePane, 75);
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
|
||||
@@ -144,8 +144,6 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
// otherwise we still consider the file as modpack zip file
|
||||
// since casually the url may not ends with ".zip"
|
||||
Path modpack = Files.createTempFile("modpack", ".zip");
|
||||
handler.resolve();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
|
||||
@@ -221,10 +221,15 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
|
||||
list.getContent().addAll(
|
||||
createTextFieldLinePane(i18n("modpack.name"), skinnable.name, new RequiredValidator()),
|
||||
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator()),
|
||||
createTextFieldLinePane(i18n("archive.version"), skinnable.version, new RequiredValidator())
|
||||
);
|
||||
|
||||
if (skinnable.options.isRequireAuthor()) {
|
||||
list.getContent().add(
|
||||
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator())
|
||||
);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.file_api"), skinnable.fileApi,
|
||||
|
||||
@@ -19,9 +19,17 @@ package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.girod.javafx.svgimage.LoaderParameters;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.girod.javafx.svgimage.ScaleQuality;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.image.apng.Png;
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
|
||||
@@ -39,7 +47,9 @@ import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -73,6 +83,46 @@ public final class ImageUtils {
|
||||
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
};
|
||||
|
||||
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
LoaderParameters parameters = new LoaderParameters();
|
||||
parameters.autoStartAnimations = false;
|
||||
|
||||
SVGImage image;
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
image = SVGLoader.load(content, parameters);
|
||||
} else {
|
||||
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
|
||||
image = CompletableFuture.supplyAsync(
|
||||
() -> SVGLoader.load(content, parameters),
|
||||
Schedulers.javafx()
|
||||
).get();
|
||||
}
|
||||
|
||||
if (image == null)
|
||||
throw new IOException("Failed to load SVG image");
|
||||
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
|
||||
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
@@ -136,11 +186,13 @@ public final class ImageUtils {
|
||||
|
||||
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
|
||||
"webp", WEBP,
|
||||
"svg", SVG,
|
||||
"apng", APNG
|
||||
);
|
||||
|
||||
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
|
||||
"image/webp", WEBP,
|
||||
"image/svg+xml", SVG,
|
||||
"image/apng", APNG
|
||||
);
|
||||
|
||||
@@ -165,6 +217,14 @@ public final class ImageUtils {
|
||||
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
|
||||
}
|
||||
|
||||
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
// This is currently a simple check, more complex checks can be considered in the future
|
||||
public static boolean isSVG(byte[] headerBuffer) {
|
||||
return headerBuffer.length > SVG_HEADER.length
|
||||
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
|
||||
}
|
||||
|
||||
private static final byte[] PNG_HEADER = {
|
||||
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
|
||||
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
|
||||
@@ -232,6 +292,8 @@ public final class ImageUtils {
|
||||
return WEBP;
|
||||
if (isApng(headerBuffer))
|
||||
return APNG;
|
||||
if (isSVG(headerBuffer))
|
||||
return SVG;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
@@ -30,6 +28,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -38,11 +37,18 @@ import java.io.InputStream;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AboutPage extends StackPane {
|
||||
public final class AboutPage extends SpinnerPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public AboutPage() {
|
||||
VBox content = new VBox();
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setContent(scrollPane);
|
||||
|
||||
ComponentList about = new ComponentList();
|
||||
{
|
||||
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
|
||||
@@ -84,27 +90,16 @@ public final class AboutPage extends StackPane {
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
|
||||
VBox content = new VBox(16);
|
||||
content.setPadding(new Insets(10));
|
||||
content.getChildren().setAll(
|
||||
ComponentList.createComponentListTitle(i18n("about")),
|
||||
about,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
|
||||
thanks,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.dependency")),
|
||||
deps,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.legal")),
|
||||
legal
|
||||
);
|
||||
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private static Image loadImage(String url) {
|
||||
|
||||
@@ -85,7 +85,13 @@ public class DownloadSettingsPage extends StackPane {
|
||||
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
|
||||
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
|
||||
var defaultAddonSourcePane = new LineSelectButton<String>();
|
||||
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
|
||||
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
|
||||
defaultAddonSourcePane.setItems("modrinth", "curseforge");
|
||||
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
|
||||
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
@@ -37,9 +36,7 @@ public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
public FeedbackPage() {
|
||||
VBox content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
@@ -84,8 +81,5 @@ public class FeedbackPage extends SpinnerPane {
|
||||
ComponentList.createComponentListTitle(i18n("contact.feedback")),
|
||||
feedback
|
||||
);
|
||||
|
||||
this.setContent(content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -42,9 +41,7 @@ public class HelpPage extends SpinnerPane {
|
||||
|
||||
public HelpPage() {
|
||||
content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
@@ -164,7 +164,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
if (skinnable.realPath == null) {
|
||||
revealButton.setDisable(true);
|
||||
|
||||
JFXButton removeButton = FXUtils.newToggleButton4(SVG.DELETE);
|
||||
JFXButton removeButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
removeButton.setOnAction(e -> skinnable.onRemove());
|
||||
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchLabel.setText(i18n("version.launch.empty"));
|
||||
currentLabel.setText(null);
|
||||
graphic.getChildren().setAll(launchLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launchNoGame());
|
||||
FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launchNoGame);
|
||||
if (tooltip == null)
|
||||
tooltip = new Tooltip(i18n("version.launch.empty.tooltip"));
|
||||
FXUtils.installFastTooltip(launchButton, tooltip);
|
||||
@@ -235,7 +235,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchLabel.setText(i18n("version.launch"));
|
||||
currentLabel.setText(currentGame);
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launch());
|
||||
FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launch);
|
||||
if (tooltip != null)
|
||||
Tooltip.uninstall(launchButton, tooltip);
|
||||
}
|
||||
@@ -277,7 +277,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private void showUpdate(boolean show) {
|
||||
doAnimation(show);
|
||||
|
||||
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
|
||||
if (show && !config().isDisableAutoShowUpdateDialog()
|
||||
&& getLatestVersion() != null
|
||||
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
|
||||
.addAction(i18n("button.view"), () -> {
|
||||
config().setPromptedVersion(getLatestVersion().getVersion());
|
||||
|
||||
@@ -18,24 +18,24 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -45,6 +45,8 @@ import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.AprilFools;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
@@ -60,20 +62,16 @@ import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class SettingsPage extends ScrollPane {
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final ToggleGroup updateChannelGroup;
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener updateListener;
|
||||
|
||||
@@ -120,82 +118,59 @@ public final class SettingsPage extends ScrollPane {
|
||||
settingsPane.getContent().add(sponsorPane);
|
||||
}
|
||||
|
||||
ObjectProperty<UpdateChannel> updateChannel;
|
||||
{
|
||||
ComponentSublist updatePane = new ComponentSublist();
|
||||
|
||||
JFXButton updateButton = new JFXButton();
|
||||
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
|
||||
updateButton.setOnAction(e -> onUpdate());
|
||||
updateButton.setPadding(Insets.EMPTY);
|
||||
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
|
||||
|
||||
var updatePane = new LineSelectButton<UpdateChannel>() {
|
||||
|
||||
{
|
||||
getStyleClass().add("update-pane");
|
||||
setNode(IDX_TRAILING, updateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTrailingTextIndex() {
|
||||
return LineComponent.IDX_TRAILING + 1;
|
||||
}
|
||||
};
|
||||
updateChannel = updatePane.valueProperty();
|
||||
updatePane.setTitle(i18n("update"));
|
||||
updatePane.setHasSubtitle(true);
|
||||
updatePane.setValue(UpdateChannel.getChannel());
|
||||
|
||||
final Label lblUpdate;
|
||||
final Label lblUpdateSub;
|
||||
{
|
||||
VBox headerLeft = new VBox();
|
||||
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
|
||||
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
|
||||
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
|
||||
|
||||
lblUpdate = new Label(i18n("update"));
|
||||
lblUpdate.getStyleClass().add("title-label");
|
||||
lblUpdateSub = new Label();
|
||||
lblUpdateSub.getStyleClass().add("subtitle-label");
|
||||
|
||||
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
|
||||
updatePane.setHeaderLeft(headerLeft);
|
||||
}
|
||||
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
|
||||
|
||||
{
|
||||
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
|
||||
btnUpdate.setOnAction(e -> onUpdate());
|
||||
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
|
||||
updateListener = any -> {
|
||||
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
||||
boolean outdated = UpdateChecker.isOutdated();
|
||||
|
||||
updateButton.setVisible(outdated);
|
||||
updateButton.setManaged(outdated);
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
|
||||
lblUpdateSub.getStyleClass().setAll("update-label");
|
||||
|
||||
lblUpdate.setText(i18n("update.found"));
|
||||
lblUpdate.getStyleClass().setAll("update-label");
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSub.setText(i18n("update.checking"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll("title-label");
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else {
|
||||
lblUpdateSub.setText(i18n("update.latest"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll("title-label");
|
||||
lblUpdateSubProperty.set(i18n("update.latest"));
|
||||
}
|
||||
};
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
|
||||
updatePane.setHeaderRight(btnUpdate);
|
||||
}
|
||||
|
||||
{
|
||||
VBox content = new VBox(12);
|
||||
content.setPadding(new Insets(8, 0, 0, 0));
|
||||
|
||||
updateChannelGroup = new ToggleGroup();
|
||||
|
||||
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
|
||||
chkUpdateStable.setUserData(UpdateChannel.STABLE);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
|
||||
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
|
||||
Label noteWrapper = new Label(i18n("update.note"));
|
||||
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
|
||||
|
||||
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
|
||||
|
||||
updatePane.getContent().add(content);
|
||||
}
|
||||
settingsPane.getContent().add(updatePane);
|
||||
}
|
||||
|
||||
@@ -205,8 +180,6 @@ public final class SettingsPage extends ScrollPane {
|
||||
previewPane.setSubtitle(i18n("update.preview.subtitle"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
|
||||
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
|
||||
updateChannel.set(UpdateChannel.getChannel());
|
||||
InvalidationListener checkUpdateListener = e -> {
|
||||
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
|
||||
};
|
||||
@@ -216,6 +189,22 @@ public final class SettingsPage extends ScrollPane {
|
||||
settingsPane.getContent().add(previewPane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
|
||||
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
|
||||
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
|
||||
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
|
||||
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
|
||||
}
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
|
||||
settingsPane.getContent().add(disableAprilFools);
|
||||
}
|
||||
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
@@ -271,6 +260,15 @@ public final class SettingsPage extends ScrollPane {
|
||||
settingsPane.getContent().add(disableAutoGameOptionsPane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton allowAutoAgentPane = new LineToggleButton();
|
||||
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
|
||||
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
|
||||
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
|
||||
|
||||
settingsPane.getContent().add(allowAutoAgentPane);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
@@ -284,12 +282,31 @@ public final class SettingsPage extends ScrollPane {
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
logButton.setOnAction(e -> onExportLogs());
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
|
||||
@@ -361,86 +378,79 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportLogs() {
|
||||
thread(() -> {
|
||||
private CompletableFuture<Path> onExportLogs() {
|
||||
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
|
||||
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
|
||||
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
|
||||
|
||||
Path outputFile;
|
||||
try {
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
for (Path path : recentLogFiles) {
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
|
||||
if ("gz".equals(extension) || "xz".equals(extension)) {
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed,
|
||||
// then copy the compressed file content as-is into a new entry in the zip file.
|
||||
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
input = "gz".equals(extension)
|
||||
? new GZIPInputStream(input)
|
||||
: new XZInputStream(input);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
IOUtils.closeQuietly(input, ex);
|
||||
input = null;
|
||||
}
|
||||
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy the log file content as-is into a new entry in the zip file.
|
||||
if ("gz".equals(extension) || "xz".equals(extension)) {
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed.
|
||||
// ensure the input file and the current zip entry are closed,
|
||||
// then copy the compressed file content as-is into a new entry in the zip file.
|
||||
|
||||
InputStream input;
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
input = "gz".equals(extension)
|
||||
? new GZIPInputStream(input)
|
||||
: new XZInputStream(input);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
continue;
|
||||
IOUtils.closeQuietly(input, ex);
|
||||
input = null;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
// Copy the log file content as-is into a new entry in the zip file.
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed.
|
||||
|
||||
InputStream input;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to export logs", e);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
|
||||
FXUtils.showFileInExplorer(outputFile);
|
||||
});
|
||||
return outputFile;
|
||||
}), Schedulers.io());
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
|
||||
@@ -22,8 +22,14 @@ import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -33,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -56,8 +63,9 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
|
||||
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString())));
|
||||
this.file = file;
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
if (type == null) {
|
||||
throw new IOException("Unknown type of file " + file);
|
||||
}
|
||||
@@ -85,14 +93,33 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
|
||||
actions.getChildren().setAll(saveButton, cancelButton);
|
||||
|
||||
Task.supplyAsync(() -> type.readAsTree(file))
|
||||
Task.supplyAsync(() -> type.read(file))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
setLoading(false);
|
||||
NBTTreeView view = new NBTTreeView(result);
|
||||
|
||||
NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file));
|
||||
var view = new TreeView<>(root) {
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
view.setCellFactory(ignored -> new NBTTreeCell());
|
||||
view.addEventHandler(TreeItem.<NBTElement>branchExpandedEvent(), event -> {
|
||||
TreeItem<NBTElement> item = event.getTreeItem();
|
||||
if (item.getValue() instanceof Tag && item.getChildren().size() == 1)
|
||||
item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
root.setExpanded(true);
|
||||
|
||||
BorderPane.setMargin(view, new Insets(10));
|
||||
onEscPressed(view, cancelButton::fire);
|
||||
root.setCenter(view);
|
||||
this.root.setCenter(view);
|
||||
} else {
|
||||
LOG.warning("Fail to open nbt file", exception);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
|
||||
|
||||
@@ -17,148 +17,34 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.IntTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import kala.compress.utils.BoundedInputStream;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.chunk.ChunkRegion;
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("dat", "dat_old") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) {
|
||||
fileInputStream.mark(3);
|
||||
byte[] header = new byte[3];
|
||||
if (fileInputStream.read(header) < 3) {
|
||||
throw new IOException("File is too small");
|
||||
}
|
||||
fileInputStream.reset();
|
||||
|
||||
InputStream input;
|
||||
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
|
||||
input = new GZIPInputStream(fileInputStream);
|
||||
} else {
|
||||
input = fileInputStream;
|
||||
}
|
||||
|
||||
Tag tag = NBTIO.readTag(input);
|
||||
if (!(tag instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + tag);
|
||||
return tag;
|
||||
}
|
||||
return NBTCodec.of().readTag(file);
|
||||
}
|
||||
},
|
||||
ANVIL("mca") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
return REGION.read(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
return REGION.readAsTree(file);
|
||||
public NBTElement read(Path file) throws IOException {
|
||||
return NBTCodec.of().readRegion(file);
|
||||
}
|
||||
},
|
||||
REGION("mcr") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) {
|
||||
ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class);
|
||||
if (r.length() == 0) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
byte[] header = new byte[4096];
|
||||
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
|
||||
Inflater inflater = new Inflater();
|
||||
|
||||
r.readFully(header);
|
||||
for (int i = 0; i < 4096; i += 4) {
|
||||
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
|
||||
int length = header[i + 3] & 0xff;
|
||||
|
||||
if (offset == 0 || length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r.seek(offset * 4096L);
|
||||
r.readFully(buffer, 0, length * 4096);
|
||||
|
||||
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
|
||||
|
||||
InputStream input = new ByteArrayInputStream(buffer);
|
||||
input.skip(5);
|
||||
input = new BoundedInputStream(input, chunkLength - 1);
|
||||
|
||||
switch (buffer[4]) {
|
||||
case 0x01:
|
||||
// GZip
|
||||
input = new GZIPInputStream(input);
|
||||
break;
|
||||
case 0x02:
|
||||
// Zlib
|
||||
inflater.reset();
|
||||
input = new InflaterInputStream(input, inflater);
|
||||
break;
|
||||
case 0x03:
|
||||
// Uncompressed
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
|
||||
}
|
||||
|
||||
try (InputStream in = input) {
|
||||
Tag chunk = NBTIO.readTag(in);
|
||||
if (!(chunk instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + chunk);
|
||||
|
||||
tag.add(chunk);
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
|
||||
|
||||
for (Tag tag : ((ListTag) item.getValue())) {
|
||||
CompoundTag chunk = (CompoundTag) tag;
|
||||
|
||||
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
|
||||
|
||||
Tag xPos = chunk.get("xPos");
|
||||
Tag zPos = chunk.get("zPos");
|
||||
|
||||
if (xPos instanceof IntTag && zPos instanceof IntTag) {
|
||||
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
|
||||
} else {
|
||||
tree.setText("Chunk: Unknown");
|
||||
}
|
||||
|
||||
item.getChildren().add(tree);
|
||||
}
|
||||
|
||||
return item;
|
||||
public ChunkRegion read(Path file) throws IOException {
|
||||
return NBTCodec.of().readRegion(file);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,7 +54,7 @@ public enum NBTFileType {
|
||||
return NBTFileType.ofFile(file) != null;
|
||||
}
|
||||
|
||||
public static NBTFileType ofFile(Path file) {
|
||||
public static @Nullable NBTFileType ofFile(Path file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
for (NBTFileType type : types) {
|
||||
for (String extension : type.extensions) {
|
||||
@@ -186,11 +72,6 @@ public enum NBTFileType {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
public abstract Tag read(Path file) throws IOException;
|
||||
public abstract NBTElement read(Path file) throws IOException;
|
||||
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
|
||||
root.setName(file.getFileName().toString());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public enum NBTTagType {
|
||||
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
|
||||
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
|
||||
STRING,
|
||||
LIST, COMPOUND;
|
||||
|
||||
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (NBTTagType type : values()) {
|
||||
lookupTable.put(type.getTagClassName(), type);
|
||||
}
|
||||
}
|
||||
|
||||
public static NBTTagType typeOf(Tag tag) {
|
||||
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown tag: " + type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private final String iconUrl;
|
||||
private final String tagClassName;
|
||||
|
||||
NBTTagType() {
|
||||
String tagName;
|
||||
String className;
|
||||
|
||||
int idx = name().indexOf('_');
|
||||
if (idx < 0) {
|
||||
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
|
||||
className = tagName + "Tag";
|
||||
} else {
|
||||
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
|
||||
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
|
||||
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
|
||||
}
|
||||
|
||||
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
|
||||
this.tagClassName = className;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public String getTagClassName() {
|
||||
return tagClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.nbt;
|
||||
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.chunk.Chunk;
|
||||
import org.glavo.nbt.chunk.ChunkRegion;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> {
|
||||
|
||||
private static @Nullable Image getIcon(NBTElement element) {
|
||||
if (element instanceof Tag tag) {
|
||||
TagType<?> type = tag.getType();
|
||||
if (type == TagType.BYTE) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png");
|
||||
} else if (type == TagType.SHORT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png");
|
||||
} else if (type == TagType.INT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png");
|
||||
} else if (type == TagType.LONG) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png");
|
||||
} else if (type == TagType.FLOAT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png");
|
||||
} else if (type == TagType.DOUBLE) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png");
|
||||
} else if (type == TagType.STRING) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png");
|
||||
} else if (type == TagType.BYTE_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png");
|
||||
} else if (type == TagType.INT_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png");
|
||||
} else if (type == TagType.LONG_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png");
|
||||
} else if (type == TagType.LIST) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
|
||||
} else if (type == TagType.COMPOUND) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (element instanceof ChunkRegion)
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
|
||||
else if (element instanceof Chunk)
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
private final ImageView imageView;
|
||||
|
||||
public NBTTreeCell() {
|
||||
imageView = new ImageView();
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
setGraphic(imageView);
|
||||
}
|
||||
|
||||
private NBTTreeItem getNBTTreeItem() {
|
||||
return (NBTTreeItem) getTreeItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(@Nullable NBTElement item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
imageView.setImage(getIcon(item));
|
||||
|
||||
String name = getNBTTreeItem().getOverrideName();
|
||||
if (name == null) {
|
||||
NBTElement value = getNBTTreeItem().getValue();
|
||||
|
||||
if (value instanceof Tag tag) {
|
||||
if (tag.getParent() instanceof ListTag<?>) {
|
||||
name = Integer.toString(tag.getIndex());
|
||||
} else {
|
||||
name = tag.getName();
|
||||
}
|
||||
} else if (value instanceof Chunk chunk) {
|
||||
name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")";
|
||||
} else {
|
||||
name = "";
|
||||
}
|
||||
}
|
||||
|
||||
String text;
|
||||
if (item instanceof ParentTag<?> parentTag) {
|
||||
text = i18n("nbt.entries", parentTag.size());
|
||||
} else if (item instanceof ValueTag<?> valueTag) {
|
||||
text = valueTag.getAsString();
|
||||
} else {
|
||||
text = null;
|
||||
}
|
||||
|
||||
if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.nbt;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.NBTParent;
|
||||
import org.glavo.nbt.chunk.Chunk;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// @author Glavo
|
||||
public final class NBTTreeItem extends TreeItem<NBTElement> {
|
||||
private final @Nullable String overrideName;
|
||||
|
||||
public NBTTreeItem(NBTElement value) {
|
||||
this(value, null);
|
||||
}
|
||||
|
||||
public NBTTreeItem(NBTElement value, @Nullable String overrideName) {
|
||||
super(value);
|
||||
this.overrideName = overrideName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return !(getValue() instanceof NBTParent<?> parent) || parent.isEmpty();
|
||||
}
|
||||
|
||||
private boolean needInitChildren = true;
|
||||
|
||||
@Override
|
||||
public ObservableList<TreeItem<NBTElement>> getChildren() {
|
||||
ObservableList<TreeItem<NBTElement>> children = super.getChildren();
|
||||
if (needInitChildren) {
|
||||
needInitChildren = false;
|
||||
|
||||
if (getValue() instanceof Chunk chunk) {
|
||||
if (chunk.getRootTag() != null) {
|
||||
children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList());
|
||||
}
|
||||
} else if (getValue() instanceof NBTParent<?> parent) {
|
||||
children.setAll(parent.stream().map(NBTTreeItem::new).toList());
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
public @Nullable String getOverrideName() {
|
||||
return overrideName;
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
if (tree != null) tree.setExpanded(true);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected javafx.scene.control.Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<Tag>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
return view -> new TreeCell<>() {
|
||||
private void setTagText(String text) {
|
||||
String name = ((Item) getTreeItem()).getName();
|
||||
|
||||
if (name == null) {
|
||||
setText(text);
|
||||
} else if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagText(int nEntries) {
|
||||
setTagText(i18n("nbt.entries", nEntries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(Tag item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
ImageView imageView = (ImageView) this.getGraphic();
|
||||
if (imageView == null) {
|
||||
imageView = new ImageView();
|
||||
this.setGraphic(imageView);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
NBTTagType tagType = NBTTagType.typeOf(item);
|
||||
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
|
||||
if (((Item) getTreeItem()).getText() != null) {
|
||||
setText(((Item) getTreeItem()).getText());
|
||||
} else {
|
||||
switch (tagType) {
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
case LONG:
|
||||
case FLOAT:
|
||||
case DOUBLE:
|
||||
case STRING:
|
||||
setTagText(item.getValue().toString());
|
||||
break;
|
||||
case BYTE_ARRAY:
|
||||
case INT_ARRAY:
|
||||
case LONG_ARRAY:
|
||||
setTagText(Array.getLength(item.getValue()));
|
||||
break;
|
||||
case LIST:
|
||||
setTagText(((ListTag) item).size());
|
||||
break;
|
||||
case COMPOUND:
|
||||
setTagText(((CompoundTag) item).size());
|
||||
break;
|
||||
default:
|
||||
setTagText(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Item buildTree(Tag tag) {
|
||||
Item item = new Item(tag);
|
||||
|
||||
if (tag instanceof CompoundTag) {
|
||||
for (Tag subTag : ((CompoundTag) tag)) {
|
||||
item.getChildren().add(buildTree(subTag));
|
||||
}
|
||||
} else if (tag instanceof ListTag) {
|
||||
int idx = 0;
|
||||
for (Tag subTag : ((ListTag) tag)) {
|
||||
Item subTree = buildTree(subTag);
|
||||
subTree.setName(String.valueOf(idx++));
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
|
||||
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public CompoundTag getRootTag() {
|
||||
return ((CompoundTag) getRoot().getValue());
|
||||
}
|
||||
|
||||
public static class Item extends TreeItem<Tag> {
|
||||
|
||||
private String text;
|
||||
private String name;
|
||||
|
||||
public Item() {
|
||||
}
|
||||
|
||||
public Item(Tag value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name == null ? getValue().getName() : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-17
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -15,21 +15,8 @@
|
||||
* 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.wizard;
|
||||
|
||||
public enum WizardNavigationResult {
|
||||
PROCEED {
|
||||
@Override
|
||||
public boolean getDeferredComputation() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
DENY {
|
||||
@Override
|
||||
public boolean getDeferredComputation() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@NotNullByDefault
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
public abstract boolean getDeferredComputation();
|
||||
}
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
@@ -195,9 +212,11 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
|
||||
useNativeGLFWPane = new LineToggleButton();
|
||||
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
|
||||
useNativeGLFWPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
useNativeOpenALPane = new LineToggleButton();
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
|
||||
+25
-25
@@ -22,7 +22,7 @@ import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.mod.Datapack;
|
||||
import org.jackhuang.hmcl.mod.DataPack;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -44,45 +44,45 @@ import java.util.regex.Pattern;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
|
||||
public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.DataPackInfoObject> implements WorldManagePage.WorldRefreshable {
|
||||
private final World world;
|
||||
private final Datapack datapack;
|
||||
private final DataPack dataPack;
|
||||
final BooleanProperty readOnly;
|
||||
|
||||
public DatapackListPage(WorldManagePage worldManagePage) {
|
||||
public DataPackListPage(WorldManagePage worldManagePage) {
|
||||
world = worldManagePage.getWorld();
|
||||
datapack = new Datapack(world.getFile().resolve("datapacks"));
|
||||
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||
dataPack = new DataPack(world.getFile().resolve("datapacks"));
|
||||
setItems(MappedObservableList.create(dataPack.getPacks(), DataPackListPageSkin.DataPackInfoObject::new));
|
||||
readOnly = worldManagePage.readOnlyProperty();
|
||||
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
||||
this::installMultiDatapack, this::refresh);
|
||||
this::installMultiDataPack, this::refresh);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void installMultiDatapack(List<Path> datapackPath) {
|
||||
datapackPath.forEach(this::installSingleDatapack);
|
||||
private void installMultiDataPack(List<Path> dataPackPath) {
|
||||
dataPackPath.forEach(this::installSingleDataPack);
|
||||
if (readOnly.get()) {
|
||||
Controllers.showToast(i18n("datapack.reload.toast"));
|
||||
}
|
||||
}
|
||||
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
private void installSingleDataPack(Path dataPack) {
|
||||
try {
|
||||
this.datapack.installPack(datapack, world.getGameVersion());
|
||||
this.dataPack.installPack(dataPack, world.getGameVersion());
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
LOG.warning("Unable to parse datapack file " + datapack, e);
|
||||
LOG.warning("Unable to parse datapack file " + dataPack, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new DatapackListPageSkin(this);
|
||||
return new DataPackListPageSkin(this);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
setLoading(true);
|
||||
Task.runAsync(datapack::loadFromDir)
|
||||
Task.runAsync(dataPack::loadFromDir)
|
||||
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
|
||||
.start();
|
||||
}
|
||||
@@ -94,18 +94,18 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null) {
|
||||
installMultiDatapack(res);
|
||||
installMultiDataPack(res);
|
||||
}
|
||||
|
||||
datapack.loadFromDir();
|
||||
dataPack.loadFromDir();
|
||||
}
|
||||
|
||||
void removeSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
|
||||
void removeSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
|
||||
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
|
||||
.forEach(pack -> {
|
||||
try {
|
||||
datapack.deletePack(pack);
|
||||
dataPack.deletePack(pack);
|
||||
} catch (IOException e) {
|
||||
// Fail to remove mods if the game is running or the datapack is absent.
|
||||
LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e);
|
||||
@@ -113,23 +113,23 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
});
|
||||
}
|
||||
|
||||
void enableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
|
||||
void enableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
|
||||
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
|
||||
.forEach(pack -> pack.setActive(true));
|
||||
}
|
||||
|
||||
void disableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
|
||||
void disableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
|
||||
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
|
||||
.forEach(pack -> pack.setActive(false));
|
||||
}
|
||||
|
||||
void openDataPackFolder() {
|
||||
FXUtils.openFolder(datapack.getPath());
|
||||
FXUtils.openFolder(dataPack.getPath());
|
||||
}
|
||||
|
||||
@NotNull Predicate<DatapackListPageSkin.DatapackInfoObject> updateSearchPredicate(String queryString) {
|
||||
@NotNull Predicate<DataPackListPageSkin.DataPackInfoObject> updateSearchPredicate(String queryString) {
|
||||
if (queryString.isBlank()) {
|
||||
return dataPack -> true;
|
||||
}
|
||||
+17
-17
@@ -42,7 +42,7 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.mod.Datapack;
|
||||
import org.jackhuang.hmcl.mod.DataPack;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -67,7 +67,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;
|
||||
|
||||
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox searchBar;
|
||||
@@ -75,8 +75,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
private final HBox selectingToolbar;
|
||||
InvalidationListener updateBarByStateWeakListener;
|
||||
|
||||
private final JFXListView<DatapackInfoObject> listView;
|
||||
private final FilteredList<DatapackInfoObject> filteredList;
|
||||
private final JFXListView<DataPackInfoObject> listView;
|
||||
private final FilteredList<DataPackInfoObject> filteredList;
|
||||
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
|
||||
@@ -85,7 +85,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
|
||||
final Consumer<Integer> toggleSelect;
|
||||
|
||||
DatapackListPageSkin(DatapackListPage skinnable) {
|
||||
DataPackListPageSkin(DataPackListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
@@ -110,7 +110,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
||||
);
|
||||
|
||||
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
@@ -181,7 +181,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
|
||||
@@ -216,13 +216,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
|
||||
static class DataPackInfoObject extends RecursiveTreeObject<DataPackInfoObject> {
|
||||
private final BooleanProperty activeProperty;
|
||||
private final Datapack.Pack packInfo;
|
||||
private final DataPack.Pack packInfo;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
DatapackInfoObject(Datapack.Pack packInfo) {
|
||||
DataPackInfoObject(DataPack.Pack packInfo) {
|
||||
this.packInfo = packInfo;
|
||||
this.activeProperty = packInfo.activeProperty();
|
||||
}
|
||||
@@ -235,7 +235,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
return packInfo.getDescription().toString();
|
||||
}
|
||||
|
||||
Datapack.Pack getPackInfo() {
|
||||
DataPack.Pack getPackInfo() {
|
||||
return packInfo;
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
@@ -286,7 +286,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
|
||||
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
@@ -298,13 +298,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
|
||||
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
|
||||
final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageContainer imageContainer = new ImageContainer(32);
|
||||
final TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
|
||||
DataPackInfoListCell(JFXListView<DataPackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
|
||||
super(listView);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
@@ -326,7 +326,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
|
||||
protected void updateControl(DataPackInfoObject dataItem, boolean empty) {
|
||||
if (empty) return;
|
||||
content.setTitle(dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
@@ -338,7 +338,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) {
|
||||
public void handleSelect(DataPackInfoListCell cell, MouseEvent mouseEvent) {
|
||||
if (cell.isEmpty()) {
|
||||
mouseEvent.consume();
|
||||
return;
|
||||
@@ -101,6 +101,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
}
|
||||
|
||||
public DownloadProvider getDownloadProvider() {
|
||||
return downloadProvider;
|
||||
}
|
||||
|
||||
public ObservableList<Node> getActions() {
|
||||
return actions;
|
||||
}
|
||||
@@ -233,18 +237,20 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
|
||||
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
private final RemoteImageLoader iconLoader;
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
iconLoader = new RemoteImageLoader(control.downloadProvider) {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris) {
|
||||
return FXUtils.getRemoteImageTask(uris, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
|
||||
@@ -27,17 +27,10 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
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.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.control.*;
|
||||
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.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
@@ -53,11 +46,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
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;
|
||||
@@ -65,14 +54,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -112,7 +94,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
setFailed(false);
|
||||
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository, page.getDownloadProvider());
|
||||
return sortVersions(versions);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
@@ -179,7 +161,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (this.callback == null) {
|
||||
saveAs(mod, file);
|
||||
} else {
|
||||
this.callback.download(version.getProfile(), version.getVersion(), mod, file);
|
||||
this.callback.download(page.getDownloadProvider(), version.getProfile(), version.getVersion(), mod, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,14 +234,12 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
|
||||
descriptionPane.getChildren().add(openMcmodButton);
|
||||
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod"));
|
||||
}
|
||||
|
||||
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
|
||||
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
|
||||
descriptionPane.getChildren().add(openUrlButton);
|
||||
openUrlButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage());
|
||||
}
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
@@ -314,25 +294,55 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.toList()) {
|
||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
continue;
|
||||
final class Versions {
|
||||
static ComponentSublist createSublist(DownloadPage control, String title, List<String> target) {
|
||||
var sublist = new ComponentSublist(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>();
|
||||
for (String gv : target) {
|
||||
List<RemoteMod.Version> versions = control.versions.get(gv);
|
||||
if (versions != null) {
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
sublist.setTitle(title);
|
||||
sublist.getStyleClass().add("no-padding");
|
||||
|
||||
return sublist;
|
||||
}
|
||||
|
||||
var sublist = new ComponentSublist(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
}
|
||||
return items;
|
||||
});
|
||||
sublist.getStyleClass().add("no-padding");
|
||||
sublist.setTitle("Minecraft " + gameVersion);
|
||||
final List<String> releases = new ArrayList<>();
|
||||
final List<String> snapshots = new ArrayList<>();
|
||||
}
|
||||
|
||||
list.getContent().add(sublist);
|
||||
TreeMap<GameVersionNumber, Versions> map = new TreeMap<>(Collections.reverseOrder());
|
||||
for (String version : control.versions.keys()) {
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(version);
|
||||
GameVersionNumber releaseOfSnapshot = GameVersionNumber.getReleaseOfSnapshot(gameVersion);
|
||||
GameVersionNumber releaseVersion = Objects.requireNonNullElse(releaseOfSnapshot, gameVersion);
|
||||
|
||||
Versions lists = map.computeIfAbsent(releaseVersion, v -> new Versions());
|
||||
|
||||
if (releaseOfSnapshot == null) {
|
||||
lists.releases.add(version);
|
||||
} else {
|
||||
lists.snapshots.add(version);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<GameVersionNumber, Versions> entry : map.entrySet()) {
|
||||
GameVersionNumber gameVersion = entry.getKey();
|
||||
Versions versions = entry.getValue();
|
||||
|
||||
if (!versions.releases.isEmpty())
|
||||
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.release", gameVersion), versions.releases));
|
||||
|
||||
if (!versions.snapshots.isEmpty())
|
||||
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.snapshot", gameVersion), versions.snapshots));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -550,7 +560,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
dependencies.put(dependency.getType(), list);
|
||||
}
|
||||
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), () -> dependency.load(selfPage.page.getDownloadProvider()))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenAcceptAsync(Schedulers.javafx(), dep -> {
|
||||
if (dep == RemoteMod.BROKEN) {
|
||||
@@ -578,7 +588,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DownloadCallback {
|
||||
void download(Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
|
||||
void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
|
||||
}
|
||||
}
|
||||
+25
-7
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -50,7 +51,7 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, CurseForgeRemoteModRepository.SHADERS, ModrinthRemoteModRepository.SHADER_PACKS);
|
||||
page.supportChinese.set(false);
|
||||
return page;
|
||||
}
|
||||
@@ -61,13 +62,30 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
repository = new Repository(type, curseForge, modrinth);
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.modrinth", "mods.curseforge");
|
||||
if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else if (curseForge != null) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
|
||||
boolean supportedCurseForge = CurseForgeRemoteModRepository.isAvailable() && curseForge != null;
|
||||
|
||||
downloadSources.setAll("mods.modrinth");
|
||||
if (supportedCurseForge) {
|
||||
downloadSources.add("mods.curseforge");
|
||||
}
|
||||
|
||||
if ("curseforge".equalsIgnoreCase(config().getDefaultAddonSource())) {
|
||||
if (supportedCurseForge) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
} else if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else if (supportedCurseForge) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -30,16 +31,18 @@ import java.util.Objects;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final List<Task<LocalModFile.ModUpdate>> dependents;
|
||||
|
||||
public ModCheckUpdatesTask(String gameVersion, Collection<LocalModFile> mods) {
|
||||
public ModCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<LocalModFile> mods) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
dependents = mods.stream().map(mod ->
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
LocalModFile.ModUpdate candidate = null;
|
||||
for (RemoteMod.Type type : RemoteMod.Type.values()) {
|
||||
LocalModFile.ModUpdate update = null;
|
||||
try {
|
||||
update = mod.checkUpdates(gameVersion, type.getRemoteModRepository());
|
||||
update = mod.checkUpdates(downloadProvider, gameVersion, type.getRemoteModRepository());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -43,6 +44,7 @@ import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -239,11 +241,12 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
if (gameVersion.isPresent()) {
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), mods);
|
||||
return new ModCheckUpdatesTask(DownloadProviders.getDownloadProvider(), gameVersion.get(), mods);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception instanceof CancellationException) return;
|
||||
if (exception != null || result == null) {
|
||||
Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
} else if (result.isEmpty()) {
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -154,7 +155,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
// Toolbar Selecting
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
@@ -465,7 +466,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
|
||||
RemoteMod remoteMod = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().getModid());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
@@ -493,7 +494,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
(profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
(downloadProvider, profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, profile, version, file, "mods")
|
||||
));
|
||||
});
|
||||
button.setDisable(false);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
|
||||
@@ -30,8 +30,10 @@ import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
@@ -39,8 +41,10 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -186,15 +190,34 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private void clearLibraries() {
|
||||
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
|
||||
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
|
||||
Task.runAsync(Schedulers.io(), () -> {
|
||||
FileUtils.deleteDirectoryQuietly(libraries);
|
||||
}).whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void clearAssets() {
|
||||
HMCLGameRepository baseDirectory = getProfile().getRepository();
|
||||
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
|
||||
if (version.get() != null) {
|
||||
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
|
||||
}
|
||||
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
|
||||
|
||||
Profile.ProfileVersion currentVersion = version.get();
|
||||
Path resourcesDir = currentVersion != null
|
||||
? getProfile().getRepository().getRunDirectory(currentVersion.getVersion()).resolve("resources")
|
||||
: null;
|
||||
|
||||
Task.runAsync(Schedulers.io(), () -> {
|
||||
FileUtils.deleteDirectoryQuietly(assetsDir);
|
||||
if (resourcesDir != null) {
|
||||
FileUtils.deleteDirectoryQuietly(resourcesDir);
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
@@ -43,6 +42,7 @@ import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.MemoryStatusBar;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -57,7 +57,6 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -115,8 +114,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
|
||||
private final BooleanProperty modpack = new SimpleBooleanProperty();
|
||||
|
||||
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
|
||||
|
||||
public VersionSettingsPage(boolean globalSetting) {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -296,30 +293,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label(i18n("settings.memory.unit.mib")));
|
||||
}
|
||||
|
||||
StackPane progressBarPane = new StackPane();
|
||||
progressBarPane.setAlignment(Pos.CENTER_LEFT);
|
||||
VBox.setMargin(progressBarPane, new Insets(8, 0, 0, 16));
|
||||
{
|
||||
progressBarPane.setMinHeight(4);
|
||||
progressBarPane.getStyleClass().add("memory-total");
|
||||
|
||||
StackPane usedMemory = new StackPane();
|
||||
usedMemory.getStyleClass().add("memory-used");
|
||||
usedMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
progressBarPane.getWidth() *
|
||||
(memoryStatus.get().getUsed() * 1.0 / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
|
||||
memoryStatus));
|
||||
StackPane allocateMemory = new StackPane();
|
||||
allocateMemory.getStyleClass().add("memory-allocate");
|
||||
allocateMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
progressBarPane.getWidth() *
|
||||
Math.min(1.0,
|
||||
(double) (HMCLGameRepository.getAllocatedMemory(maxMemory.get() * 1024L * 1024L, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())
|
||||
+ memoryStatus.get().getUsed()) / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
|
||||
maxMemory, memoryStatus, chkAutoAllocate.selectedProperty()));
|
||||
|
||||
progressBarPane.getChildren().setAll(allocateMemory, usedMemory);
|
||||
}
|
||||
MemoryStatusBar memoryStatusBar = new MemoryStatusBar();
|
||||
VBox.setMargin(memoryStatusBar, new Insets(8, 0, 0, 16));
|
||||
memoryStatusBar.memoryAllocatedProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
(double) HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemory.get() * 1024L * 1024L,
|
||||
memoryStatusBar.getMemoryStatus().getAvailable(),
|
||||
chkAutoAllocate.isSelected()),
|
||||
memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
|
||||
BorderPane digitalPane = new BorderPane();
|
||||
VBox.setMargin(digitalPane, new Insets(0, 0, 0, 16));
|
||||
@@ -327,26 +308,29 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
Label lblPhysicalMemory = new Label();
|
||||
lblPhysicalMemory.getStyleClass().add("memory-label");
|
||||
digitalPane.setLeft(lblPhysicalMemory);
|
||||
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() ->
|
||||
i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getTotal())), memoryStatus));
|
||||
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
|
||||
return i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getTotal()));
|
||||
}, memoryStatusBar.memoryStatusProperty()));
|
||||
|
||||
Label lblAllocateMemory = new Label();
|
||||
lblAllocateMemory.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
|
||||
long maxMemory = Lang.parseInt(this.maxMemory.get(), 0) * 1024L * 1024L;
|
||||
return i18n(memoryStatus.get().hasAvailable() && maxMemory > memoryStatus.get().getAvailable()
|
||||
return i18n(memoryStatus.hasAvailable() && maxMemory > memoryStatus.getAvailable()
|
||||
? (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto.exceeded" : "settings.memory.allocate.manual.exceeded")
|
||||
: (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto" : "settings.memory.allocate.manual"),
|
||||
GIGABYTES.convertFromBytes(maxMemory),
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getAvailable()));
|
||||
}, memoryStatus, maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.getAvailable(), chkAutoAllocate.isSelected())),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getAvailable()));
|
||||
}, memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
lblAllocateMemory.getStyleClass().add("memory-label");
|
||||
digitalPane.setRight(lblAllocateMemory);
|
||||
}
|
||||
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, memoryStatusBar, digitalPane);
|
||||
}
|
||||
|
||||
launcherVisibilityPane = new LineSelectButton<>();
|
||||
@@ -472,7 +456,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
componentList.disableProperty().bind(enableSpecificSettings.not());
|
||||
}
|
||||
|
||||
|
||||
private void fireWorkingDirChanged() {
|
||||
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
|
||||
}
|
||||
@@ -776,57 +760,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static final class UpdateMemoryStatus extends Thread {
|
||||
|
||||
@FXThread
|
||||
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
|
||||
|
||||
@FXThread
|
||||
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
if (memoryStatusPropertyCache != null) {
|
||||
var property = memoryStatusPropertyCache.get();
|
||||
if (property != null) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
memoryStatusPropertyCache = new WeakReference<>(property);
|
||||
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
|
||||
return property;
|
||||
}
|
||||
|
||||
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
|
||||
|
||||
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
|
||||
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
|
||||
|
||||
setName("UpdateMemoryStatus");
|
||||
setDaemon(true);
|
||||
setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
|
||||
|
||||
var memoryStatusProperty = memoryStatusPropertyRef.get();
|
||||
if (memoryStatusProperty == null)
|
||||
return;
|
||||
|
||||
if (Controllers.isStopped())
|
||||
return;
|
||||
|
||||
Platform.runLater(() -> memoryStatusProperty.set(status));
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import javafx.application.Platform;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@@ -42,13 +43,13 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
@@ -72,11 +73,11 @@ public final class Versions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void downloadModpackImpl(Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
|
||||
public static void downloadModpackImpl(DownloadProvider downloadProvider, Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
|
||||
Path modpack;
|
||||
URI downloadURL;
|
||||
List<URI> downloadURLs;
|
||||
try {
|
||||
downloadURL = NetworkUtils.toURI(file.getFile().getUrl());
|
||||
downloadURLs = downloadProvider.injectURLWithCandidates(file.getFile().getUrl());
|
||||
modpack = Files.createTempFile("modpack", ".zip");
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Controllers.dialog(
|
||||
@@ -85,7 +86,7 @@ public final class Versions {
|
||||
return;
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(downloadURL, modpack)
|
||||
new FileDownloadTask(downloadURLs, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
ModpackInstallWizardProvider installWizardProvider;
|
||||
|
||||
@@ -259,7 +259,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
|
||||
btnReveal.setOnAction(event -> skinnable.onReveal());
|
||||
|
||||
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE);
|
||||
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
right.getChildren().add(btnDelete);
|
||||
FXUtils.installFastTooltip(btnDelete, i18n("world.backup.delete"));
|
||||
btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::onDelete, null));
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -34,9 +33,8 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -66,7 +64,8 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
private final WorldManagePage worldManagePage;
|
||||
private boolean isReadOnly;
|
||||
private final World world;
|
||||
private CompoundTag levelDat;
|
||||
private CompoundTag levelData;
|
||||
private CompoundTag playerData;
|
||||
|
||||
private final ImageContainer iconImageView = new ImageContainer(32);
|
||||
|
||||
@@ -76,15 +75,9 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
refresh();
|
||||
}
|
||||
|
||||
private CompoundTag loadWorldInfo() throws IOException {
|
||||
if (!Files.isDirectory(world.getFile()))
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
return world.getLevelData();
|
||||
}
|
||||
|
||||
private void updateControls() {
|
||||
CompoundTag dataTag = levelDat.get("Data");
|
||||
CompoundTag dataTag = (CompoundTag) levelData.get("Data");
|
||||
CompoundTag playerTag = playerData;
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -107,7 +100,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
|
||||
var worldName = new SimpleStringProperty(worldNameTag.getValue());
|
||||
var worldName = new SimpleStringProperty(worldNameTag.get());
|
||||
FXUtils.bindString(worldNameField, worldName);
|
||||
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
|
||||
worldName.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -205,12 +198,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
worldSpawnPoint.setTitle(i18n("world.info.spawn"));
|
||||
|
||||
String value;
|
||||
// Valid after 1.21.9-pre1
|
||||
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
|
||||
value = Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
? dimensionTag
|
||||
: new StringTag("SpawnDimension", "minecraft:overworld"))
|
||||
.formatPosition(posTag);
|
||||
} else if (dataTag.get("SpawnX") instanceof IntTag intX
|
||||
value = (spawnTag.get("dimension") instanceof StringTag dimensionTag)
|
||||
? Dimension.of(dimensionTag).formatPosition(posTag)
|
||||
: Dimension.OVERWORLD.formatPosition(posTag);
|
||||
}
|
||||
// Valid before 1.21.9-pre1
|
||||
else if (dataTag.get("SpawnX") instanceof IntTag intX
|
||||
&& dataTag.get("SpawnY") instanceof IntTag intY
|
||||
&& dataTag.get("SpawnZ") instanceof IntTag intZ) {
|
||||
value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||
@@ -248,34 +243,51 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
{
|
||||
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
|
||||
generateFeaturesButton.setDisable(isReadOnly);
|
||||
|
||||
// generate_features was valid after 20w20a and MapFeatures was before that
|
||||
if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) {
|
||||
bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton);
|
||||
// Valid before (1.16)20w20a
|
||||
if (dataTag.get("MapFeatures") instanceof ByteTag generateFeaturesTag) {
|
||||
bindTagAndToggleButton(generateFeaturesTag, generateFeaturesButton);
|
||||
}
|
||||
// Valid after (1.16)20w20a
|
||||
else if (world.getNormalizedWorldGenSettingsData() != null) {
|
||||
CompoundTag unifiedWorldGenSettingsDataTag = world.getNormalizedWorldGenSettingsData();
|
||||
Tag tag = unifiedWorldGenSettingsDataTag.get("generate_features"); // Valid between (1.16)20w20a and 26.1-snapshot-6
|
||||
if (tag == null)
|
||||
tag = unifiedWorldGenSettingsDataTag.get("generate_structures"); // Valid after 26.1-snapshot-6
|
||||
bindTagAndToggleButton(tag, generateFeaturesButton);
|
||||
} else {
|
||||
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
|
||||
generateFeaturesButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
var difficultyButton = new LineSelectButton<Difficulty>();
|
||||
{
|
||||
difficultyButton.setTitle(i18n("world.info.difficulty"));
|
||||
difficultyButton.setDisable(worldManagePage.isReadOnly());
|
||||
difficultyButton.setDisable(isReadOnly);
|
||||
difficultyButton.setItems(Difficulty.items);
|
||||
|
||||
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
|
||||
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
|
||||
if (difficulty != null) {
|
||||
difficultyButton.setValue(difficulty);
|
||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
difficultyTag.setValue((byte) newValue.ordinal());
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
difficultyButton.setDisable(true);
|
||||
}
|
||||
Difficulty difficulty;
|
||||
// Valid before 26.1-snapshot-6
|
||||
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag
|
||||
&& (difficulty = Difficulty.of(difficultyTag.getValue())) != null) {
|
||||
difficultyButton.setValue(difficulty);
|
||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
difficultyTag.setValue((byte) newValue.ordinal());
|
||||
saveWorldData();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Valid after 26.1-snapshot-6
|
||||
else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag
|
||||
&& difficultySettingTag.get("difficulty") instanceof StringTag difficultyTag
|
||||
&& (difficulty = Difficulty.of(difficultyTag.getValue())) != null) {
|
||||
difficultyButton.setValue(difficulty);
|
||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
difficultyTag.setValue(newValue.getTagStringValue());
|
||||
saveWorldData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
difficultyButton.setDisable(true);
|
||||
}
|
||||
@@ -285,8 +297,17 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
{
|
||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||
difficultyLockPane.setDisable(isReadOnly);
|
||||
|
||||
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
|
||||
// Valid before 26.1-snapshot-6
|
||||
if (dataTag.get("DifficultyLocked") instanceof ByteTag difficultyLockedTag) {
|
||||
bindTagAndToggleButton(difficultyLockedTag, difficultyLockPane);
|
||||
}
|
||||
// Valid after 26.1-snapshot-6
|
||||
else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag
|
||||
&& difficultySettingTag.get("locked") instanceof ByteTag lockedTag) {
|
||||
bindTagAndToggleButton(lockedTag, difficultyLockPane);
|
||||
} else {
|
||||
difficultyLockPane.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
worldInfo.getContent().setAll(
|
||||
@@ -296,14 +317,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
|
||||
}
|
||||
|
||||
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
|
||||
if (playerTag != null) {
|
||||
ComponentList playerInfo = new ComponentList();
|
||||
|
||||
var locationPane = new LineTextPane();
|
||||
{
|
||||
locationPane.setTitle(i18n("world.info.player.location"));
|
||||
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag<?> posTag) {
|
||||
locationPane.setText(dimension.formatPosition(posTag));
|
||||
}
|
||||
}
|
||||
@@ -326,7 +347,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
|
||||
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
|
||||
&& intArrayTag.length() >= 3) { // Valid after 25w07a
|
||||
&& intArrayTag.size() >= 3) { // Valid after 25w07a
|
||||
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
|
||||
} else if (playerTag.get("SpawnX") instanceof IntTag intX
|
||||
&& playerTag.get("SpawnY") instanceof IntTag intY
|
||||
@@ -343,8 +364,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
|
||||
playerGameTypePane.setItems(GameType.items);
|
||||
|
||||
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag
|
||||
&& dataTag.get("hardcore") instanceof ByteTag hardcoreTag) {
|
||||
// Valid before 26.1-snapshot-6
|
||||
Tag rawHardcoreTag = dataTag.get("hardcore");
|
||||
// Valid after 26.1-snapshot-6
|
||||
if (rawHardcoreTag == null && dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingsTag) {
|
||||
rawHardcoreTag = difficultySettingsTag.get("hardcore");
|
||||
}
|
||||
|
||||
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag && rawHardcoreTag instanceof ByteTag hardcoreTag) {
|
||||
boolean isHardcore = hardcoreTag.getValue() == 1;
|
||||
GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore);
|
||||
if (gameType != null) {
|
||||
@@ -358,7 +385,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
playerGameTypeTag.setValue(newValue.ordinal());
|
||||
hardcoreTag.setValue((byte) 0);
|
||||
}
|
||||
saveLevelDat();
|
||||
saveWorldData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -428,10 +455,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
try {
|
||||
byteTag.setValue((byte) (newValue ? 1 : 0));
|
||||
saveLevelDat();
|
||||
saveWorldData();
|
||||
} catch (Exception e) {
|
||||
toggleButton.setSelected(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
LOG.warning("Exception happened when saving world info", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -451,11 +478,11 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
Integer integer = Lang.toIntOrNull(newValue);
|
||||
if (integer != null) {
|
||||
intTag.setValue(integer);
|
||||
saveLevelDat();
|
||||
saveWorldData();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
LOG.warning("Exception happened when saving world data", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -472,11 +499,11 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
Float floatValue = Lang.toFloatOrNull(newValue);
|
||||
if (floatValue != null) {
|
||||
floatTag.setValue(floatValue);
|
||||
saveLevelDat();
|
||||
saveWorldData();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
LOG.warning("Exception happened when saving world data", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -484,30 +511,27 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void saveLevelDat() {
|
||||
LOG.info("Saving level.dat of world " + world.getWorldName());
|
||||
private void saveWorldData() {
|
||||
LOG.info("Saving data of world " + world.getWorldName());
|
||||
try {
|
||||
this.world.writeLevelDat(levelDat);
|
||||
this.world.writeWorldData();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save level.dat of world " + world.getWorldName(), e);
|
||||
LOG.warning("Failed to save world data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
this.isReadOnly = worldManagePage.isReadOnly();
|
||||
this.setLoading(true);
|
||||
Task.supplyAsync(this::loadWorldInfo)
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
if (exception == null) {
|
||||
this.levelDat = result;
|
||||
updateControls();
|
||||
setLoading(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load level.dat", exception);
|
||||
setFailedReason(i18n("world.info.failed"));
|
||||
}
|
||||
})).start();
|
||||
setFailedReason(null);
|
||||
try {
|
||||
this.isReadOnly = worldManagePage.isReadOnly();
|
||||
this.levelData = world.getLevelData();
|
||||
this.playerData = world.getPlayerData();
|
||||
updateControls();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to refresh world info", e);
|
||||
setFailedReason(i18n("world.info.failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private record Dimension(String name) {
|
||||
@@ -517,7 +541,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
static Dimension of(Tag tag) {
|
||||
if (tag instanceof IntTag intTag) {
|
||||
return switch (intTag.getValue()) {
|
||||
return switch (intTag.get()) {
|
||||
case 0 -> OVERWORLD;
|
||||
case -1 -> THE_NETHER;
|
||||
case 1 -> THE_END;
|
||||
@@ -537,30 +561,23 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
|
||||
String formatPosition(Tag tag) {
|
||||
if (tag instanceof ListTag listTag && listTag.size() == 3) {
|
||||
if (tag instanceof ListTag<?> listTag && listTag.size() == 3) {
|
||||
|
||||
Tag x = listTag.get(0);
|
||||
Tag y = listTag.get(1);
|
||||
Tag z = listTag.get(2);
|
||||
|
||||
if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue())
|
||||
: String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue());
|
||||
if (listTag.getTag(0) instanceof DoubleTag x
|
||||
&& listTag.getTag(1) instanceof DoubleTag y
|
||||
&& listTag.getTag(2) instanceof DoubleTag z) {
|
||||
return formatPosition(x.get(), y.get(), z.get());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tag instanceof IntArrayTag intArrayTag) {
|
||||
int x = intArrayTag.get(0);
|
||||
int y = intArrayTag.get(1);
|
||||
int z = intArrayTag.get(2);
|
||||
|
||||
int x = intArrayTag.getValue(0);
|
||||
int y = intArrayTag.getValue(1);
|
||||
int z = intArrayTag.getValue(2);
|
||||
|
||||
return this == OVERWORLD
|
||||
? String.format("(%d, %d, %d)", x, y, z)
|
||||
: String.format("%s (%d, %d, %d)", name, x, y, z);
|
||||
return formatPosition(x, y, z);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -568,14 +585,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
String formatPosition(int x, int y, int z) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%d, %d, %d)", x, y, z)
|
||||
: String.format("%s (%d, %d, %d)", name, x, y, z);
|
||||
? "(%d, %d, %d)".formatted(x, y, z)
|
||||
: "%s (%d, %d, %d)".formatted(name, x, y, z);
|
||||
}
|
||||
|
||||
String formatPosition(double x, double y, double z) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%.2f, %.2f, %.2f)", x, y, z)
|
||||
: String.format("%s (%.2f, %.2f, %.2f)", name, x, y, z);
|
||||
? "(%.2f, %.2f, %.2f)".formatted(x, y, z)
|
||||
: "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,6 +605,15 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
return (d >= 0 && d < items.size()) ? items.get(d) : null;
|
||||
}
|
||||
|
||||
static Difficulty of(String name) {
|
||||
for (Difficulty d : items) if (d.name().toLowerCase(Locale.ROOT).equals(name)) return d;
|
||||
return null;
|
||||
}
|
||||
|
||||
String getTagStringValue() {
|
||||
return this.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return i18n("world.info.difficulty." + name().toLowerCase(Locale.ROOT));
|
||||
@@ -653,7 +679,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
Files.deleteIfExists(output);
|
||||
iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete world icon " + e.getMessage());
|
||||
LOG.warning("Failed to delete world icon ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,7 +382,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
||||
exportMenuItem.setDisable(worldLocked);
|
||||
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> page.delete(world), popup);
|
||||
deleteMenuItem.setDisable(worldLocked);
|
||||
|
||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
|
||||
|
||||
@@ -67,7 +67,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final TabHeader header = new TabHeader(transitionPane);
|
||||
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
||||
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
|
||||
|
||||
public WorldManagePage(World world, Profile profile, String instanceId) {
|
||||
this.world = world;
|
||||
@@ -78,15 +78,15 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
updateSessionLockChannel();
|
||||
|
||||
try {
|
||||
this.world.reloadLevelDat();
|
||||
this.world.reloadWorldData();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
|
||||
LOG.warning("Can not load world data of world: " + this.world.getFile(), e);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
|
||||
}
|
||||
|
||||
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
dataPackTab.setNodeSupplier(() -> new DataPackListPage(this));
|
||||
|
||||
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||
|
||||
@@ -106,9 +106,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
public void refresh() {
|
||||
updateSessionLockChannel();
|
||||
try {
|
||||
world.reloadLevelDat();
|
||||
world.reloadWorldData();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||
LOG.warning("Can not load world data of world: " + world.getFile(), e);
|
||||
closePageForLoadingFail();
|
||||
return;
|
||||
}
|
||||
@@ -218,9 +218,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (getSkinnable().world.supportDatapacks()) {
|
||||
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
if (getSkinnable().world.supportDataPacks()) {
|
||||
getSkinnable().header.getTabs().add(getSkinnable().dataPackTab);
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().dataPackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup)
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -66,7 +67,7 @@ public final class UpdateHandler {
|
||||
performMigration();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to perform migration", e);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -81,7 +82,7 @@ public final class UpdateHandler {
|
||||
applyUpdate(Paths.get(args[1]));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to apply update", e);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -174,15 +175,25 @@ public final class UpdateHandler {
|
||||
startJava(updateTo, "--apply-to", self.toString());
|
||||
}
|
||||
|
||||
private static void startJava(Path jar, String... appArgs) throws IOException {
|
||||
public static void startJava(Path jar, String... appArgs) throws IOException {
|
||||
List<String> commandline = new ArrayList<>();
|
||||
commandline.add(JavaRuntime.getDefault().getBinary().toString());
|
||||
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
|
||||
Object key = entry.getKey();
|
||||
if (key instanceof String && ((String) key).startsWith("hmcl.")) {
|
||||
commandline.add("-D" + key + "=" + entry.getValue());
|
||||
|
||||
try {
|
||||
for (String inputArgument : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
|
||||
if (inputArgument.startsWith("-D") || inputArgument.startsWith("-X")) {
|
||||
commandline.add(inputArgument);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// ManagementFactory not available
|
||||
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
|
||||
if (entry.getKey() instanceof String key && key.startsWith("hmcl.")) {
|
||||
commandline.add("-D" + key + "=" + entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandline.add("-jar");
|
||||
commandline.add(jar.toAbsolutePath().toString());
|
||||
commandline.addAll(Arrays.asList(appArgs));
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/// April Fools' Day utilities.
|
||||
///
|
||||
/// This class provides methods to check if it is April Fools' Day or near April Fools' Day.
|
||||
/// It also provides a method to check if April Fools is enabled.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class AprilFools {
|
||||
|
||||
private static final boolean ENABLED;
|
||||
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;
|
||||
|
||||
static {
|
||||
var date = LocalDate.now();
|
||||
|
||||
// Some countries/regions may oppose April Fools' Day for various reasons.
|
||||
// Therefore, we use a regional whitelist to avoid risks.
|
||||
// Currently, we have only listed a limited set of countries/regions for testing.
|
||||
// We will investigate more countries/regions in the future to expand this list.
|
||||
boolean supportedRegion = List.of(
|
||||
"CN", "TW", "HK", "MO", "JP", "KR", "VN", "SG", "MY",
|
||||
"ES", "DE", "FR", "GB", "RU", "UA", "US"
|
||||
).contains(LocaleUtils.SYSTEM_DEFAULT.getCountry());
|
||||
|
||||
boolean aprilFoolsMode;
|
||||
String value = System.getProperty("hmcl.april_fools", System.getenv("HMCL_APRIL_FOOLS"));
|
||||
if ("true".equalsIgnoreCase(value))
|
||||
aprilFoolsMode = true;
|
||||
else if ("false".equalsIgnoreCase(value) || !supportedRegion)
|
||||
aprilFoolsMode = false;
|
||||
else
|
||||
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
|
||||
|
||||
ENABLED = aprilFoolsMode && !config().isDisableAprilFools();
|
||||
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
|
||||
}
|
||||
|
||||
/// Whether April Fools settings should be shown.
|
||||
///
|
||||
/// This method returns true if April Fools settings should be shown.
|
||||
public static boolean isShowAprilFoolsSettings() {
|
||||
return SHOW_APRIL_FOOLS_SETTINGS;
|
||||
}
|
||||
|
||||
/// Whether April Fools is enabled.
|
||||
///
|
||||
/// This method returns true if April Fools is enabled.
|
||||
public static boolean isEnabled() {
|
||||
return ENABLED;
|
||||
}
|
||||
|
||||
private AprilFools() {
|
||||
}
|
||||
}
|
||||
@@ -31,21 +31,27 @@ public final class ChunkBaseApp {
|
||||
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
|
||||
|
||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
||||
"1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13",
|
||||
"1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
|
||||
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
|
||||
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
|
||||
"1.20", "1.19.3", "1.19", "1.18", "1.16", "1.13", "1.9", "1.7"
|
||||
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
|
||||
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
|
||||
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] NETHER_FORTRESS_GAME_VERSIONS = {
|
||||
"1.18", "1.16", "1.7"
|
||||
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
|
||||
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
|
||||
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] END_CITY_GAME_VERSIONS = {
|
||||
"1.19", "1.13"
|
||||
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
|
||||
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
|
||||
"1.13"
|
||||
};
|
||||
|
||||
public static boolean isSupported(@NotNull World world) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -29,18 +30,21 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
public final class FileSaver extends Thread {
|
||||
|
||||
private static final Pair<Path, String> SHUTDOWN = Pair.pair(null, null);
|
||||
|
||||
private static final BlockingQueue<Pair<Path, String>> queue = new LinkedBlockingQueue<>();
|
||||
private static final BlockingQueue<Action> queue = new LinkedBlockingQueue<>();
|
||||
private static final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private static final ReentrantLock runningLock = new ReentrantLock();
|
||||
private static volatile boolean shutdown = false;
|
||||
|
||||
private static void addAction(Action action) {
|
||||
queue.add(action);
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new FileSaver().start();
|
||||
}
|
||||
}
|
||||
|
||||
private static void doSave(Map<Path, String> map) {
|
||||
for (Map.Entry<Path, String> entry : map.entrySet()) {
|
||||
saveSync(entry.getKey(), entry.getValue());
|
||||
@@ -53,10 +57,7 @@ public final class FileSaver extends Thread {
|
||||
|
||||
ShutdownHook.ensureInstalled();
|
||||
|
||||
queue.add(Pair.pair(file, content));
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new FileSaver().start();
|
||||
}
|
||||
addAction(new DoSave(file, content));
|
||||
}
|
||||
|
||||
public static void saveSync(Path file, String content) {
|
||||
@@ -70,7 +71,17 @@ public final class FileSaver extends Thread {
|
||||
|
||||
public static void shutdown() {
|
||||
shutdown = true;
|
||||
queue.add(SHUTDOWN);
|
||||
queue.add(Shutdown.INSTANCE);
|
||||
}
|
||||
|
||||
/// Wait for all saves to complete.
|
||||
///
|
||||
/// This method should be called before the [#shutdown()] method.
|
||||
public static void waitForAllSaves() throws InterruptedException {
|
||||
assert !shutdown;
|
||||
Wait wait = new Wait();
|
||||
addAction(wait);
|
||||
wait.await();
|
||||
}
|
||||
|
||||
private FileSaver() {
|
||||
@@ -92,34 +103,46 @@ public final class FileSaver extends Thread {
|
||||
runningLock.lock();
|
||||
try {
|
||||
HashMap<Path, String> map = new HashMap<>();
|
||||
ArrayList<Pair<Path, String>> buffer = new ArrayList<>();
|
||||
ArrayList<Action> buffer = new ArrayList<>();
|
||||
ArrayList<Wait> waits = new ArrayList<>();
|
||||
|
||||
while (!stopped) {
|
||||
if (shutdown) {
|
||||
stopCurrentSaver();
|
||||
} else {
|
||||
Pair<Path, String> head = queue.poll(30, TimeUnit.SECONDS);
|
||||
if (head == null || head == SHUTDOWN) {
|
||||
stopCurrentSaver();
|
||||
} else {
|
||||
map.put(head.getKey(), head.getValue());
|
||||
Action head = queue.poll(30, TimeUnit.SECONDS);
|
||||
if (head instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(200); // Waiting for more changes
|
||||
} else if (head instanceof Wait wait) {
|
||||
waits.add(wait);
|
||||
} else if (head == null || head instanceof Shutdown) {
|
||||
// Shutdown or timeout
|
||||
stopCurrentSaver();
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.drainTo(buffer) > 0) {
|
||||
for (Pair<Path, String> pair : buffer) {
|
||||
if (pair == SHUTDOWN)
|
||||
for (Action action : buffer) {
|
||||
if (action instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
} else if (action instanceof Wait wait) {
|
||||
waits.add(wait);
|
||||
} else if (action instanceof Shutdown) {
|
||||
stopCurrentSaver();
|
||||
else
|
||||
map.put(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
doSave(map);
|
||||
map.clear();
|
||||
|
||||
for (Wait wait : waits) {
|
||||
wait.countDown();
|
||||
}
|
||||
waits.clear();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError("This thread cannot be interrupted", e);
|
||||
@@ -128,6 +151,28 @@ public final class FileSaver extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface Action {
|
||||
}
|
||||
|
||||
private record DoSave(Path file, String content) implements Action {
|
||||
}
|
||||
|
||||
private static final class Wait implements Action {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
public void await() throws InterruptedException {
|
||||
latch.await();
|
||||
}
|
||||
|
||||
public void countDown() {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private enum Shutdown implements Action {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
private static final class ShutdownHook extends Thread {
|
||||
|
||||
static {
|
||||
@@ -144,9 +189,10 @@ public final class FileSaver extends Thread {
|
||||
runningLock.lock();
|
||||
try {
|
||||
HashMap<Path, String> map = new HashMap<>();
|
||||
for (Pair<Path, String> pair : queue) {
|
||||
if (pair != SHUTDOWN)
|
||||
map.put(pair.getKey(), pair.getValue());
|
||||
for (Action action : queue) {
|
||||
if (action instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
}
|
||||
}
|
||||
doSave(map);
|
||||
} finally {
|
||||
|
||||
@@ -63,6 +63,16 @@ public final class NativePatcher {
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/LWJGL/lwjgl3/issues/1111
|
||||
public static boolean needPatchMemoryUtil(Version version, int javaVersion) {
|
||||
return javaVersion >= 25 && javaVersion <= 26 && version.getLibraries().stream().anyMatch(library ->
|
||||
"org.lwjgl".equals(library.getGroupId())
|
||||
&& "lwjgl".equals(library.getArtifactId())
|
||||
&& "3.4.1".equals(library.getVersion())
|
||||
&& library.getClassifier() == null
|
||||
);
|
||||
}
|
||||
|
||||
public static Version patchNative(DefaultGameRepository repository,
|
||||
Version version, String gameVersion,
|
||||
JavaRuntime javaVersion,
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
@@ -33,18 +34,20 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class RemoteImageLoader {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
|
||||
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
|
||||
|
||||
public RemoteImageLoader() {
|
||||
public RemoteImageLoader(DownloadProvider downloadProvider) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
}
|
||||
|
||||
protected @Nullable Image getPlaceholder() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull URI uri);
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris);
|
||||
|
||||
@FXThread
|
||||
public void load(@NotNull WritableValue<Image> writableValue, String url) {
|
||||
@@ -82,7 +85,7 @@ public abstract class RemoteImageLoader {
|
||||
}
|
||||
}
|
||||
|
||||
createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
createLoadTask(downloadProvider.injectURLWithCandidates(url)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Image image;
|
||||
if (exception == null) {
|
||||
image = result;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Restarter {
|
||||
|
||||
/// Restart the current application.
|
||||
public static void restartSelf() throws IOException {
|
||||
LOG.info("Restarting HMCL");
|
||||
|
||||
Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null) {
|
||||
throw new IOException("Failed to find current HMCL location");
|
||||
}
|
||||
|
||||
UpdateHandler.startJava(thisJar);
|
||||
}
|
||||
|
||||
private Restarter() {
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,32 @@ public class Translator_lzh extends Translator {
|
||||
case "23w13a_or_b" -> "二三週一三暨";
|
||||
case "24w14potato" -> "二四週一四芋";
|
||||
case "25w14craftmine" -> "二五週一四礦";
|
||||
default -> version;
|
||||
default -> {
|
||||
if (version.length() >= 6 && version.charAt(2) == 'w') {
|
||||
Matcher matcher = Pattern.compile("(?<year>[0-9]{2})w(?<week>[0-9]{2})(?<suffix>.+)").matcher(version);
|
||||
if (matcher.matches()) {
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int week = Integer.parseInt(matcher.group("week"));
|
||||
String suffix = matcher.group("suffix");
|
||||
char suffixFirstChar = suffix.charAt(0);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendDigitByDigit(builder, String.valueOf(year));
|
||||
builder.append('週');
|
||||
appendDigitByDigit(builder, String.valueOf(week));
|
||||
|
||||
if (suffix.length() == 1
|
||||
&& suffixFirstChar >= 'a' && (suffixFirstChar - 'a') < TIAN_GAN.length)
|
||||
builder.append(TIAN_GAN[suffixFirstChar - 'a']);
|
||||
else
|
||||
builder.append(suffix);
|
||||
|
||||
yield builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
yield version;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return gameVersion.toString();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
{
|
||||
"title" : "jsoup",
|
||||
"subtitle" : "Copyright © 2009 - 2024 Jonathan Hedley (https://jsoup.org/)\nLicensed under the MIT License.",
|
||||
"subtitle" : "Copyright © 2009 - 2025 Jonathan Hedley (https://jsoup.org/)\nLicensed under the MIT License.",
|
||||
"externalLink" : "https://jsoup.org/"
|
||||
},
|
||||
{
|
||||
@@ -45,9 +45,9 @@
|
||||
"externalLink" : "https://github.com/jenkinsci/constant-pool-scanner"
|
||||
},
|
||||
{
|
||||
"title" : "OpenNBT",
|
||||
"subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.",
|
||||
"externalLink" : "https://github.com/GeyserMC/OpenNBT"
|
||||
"title" : "HelloNBT",
|
||||
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/Glavo/HelloNBT"
|
||||
},
|
||||
{
|
||||
"title" : "minecraft-jfx-skin",
|
||||
@@ -66,17 +66,22 @@
|
||||
},
|
||||
{
|
||||
"title" : "Java Animated PNG",
|
||||
"subtitle" : "Copyright (C) 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
|
||||
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/aellerton/japng"
|
||||
},
|
||||
{
|
||||
"title" : "fxsvgimage",
|
||||
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
|
||||
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
|
||||
},
|
||||
{
|
||||
"title": "Terracotta",
|
||||
"subtitle": "Copyright (C) 2025 Burning_TNT.\nAll rights reserved.",
|
||||
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",
|
||||
"externalLink": "https://github.com/burningtnt/Terracotta"
|
||||
},
|
||||
{
|
||||
"title": "EasyTier",
|
||||
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
|
||||
"subtitle": "Copyright © 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
|
||||
"externalLink": "https://github.com/EasyTier/EasyTier"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
.scroll-bar .thumb {
|
||||
-fx-fill: -monet-surface-tint;
|
||||
-fx-arc-width: 0;
|
||||
-fx-arc-height: 0;
|
||||
-fx-arc-width: 8;
|
||||
-fx-arc-height: 8;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
@@ -113,28 +113,26 @@
|
||||
.memory-label {
|
||||
}
|
||||
|
||||
.memory-used {
|
||||
-fx-background-color: -monet-primary;
|
||||
.memory-status-bar {
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.memory-used:disabled {
|
||||
.memory-status-bar:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.memory-allocate {
|
||||
-fx-background-color: -monet-primary-transparent-50;
|
||||
.memory-status-bar .track {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
-fx-background-radius: 2;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
.memory-allocate:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
.memory-status-bar .memory-used {
|
||||
-fx-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.memory-total {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.memory-total:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
.memory-status-bar .memory-allocate {
|
||||
-fx-fill: -monet-primary-transparent-50;
|
||||
}
|
||||
|
||||
.update-label {
|
||||
@@ -445,6 +443,7 @@
|
||||
-fx-max-height: 55px;
|
||||
-fx-min-height: 55px;
|
||||
-fx-background-color: -monet-primary-container;
|
||||
-fx-opacity: 1; /* Override the opacity of disabled button */
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
@@ -526,7 +525,8 @@
|
||||
}
|
||||
|
||||
.jfx-dialog-layout {
|
||||
-fx-padding: 24.0px 24.0px 16.0px 24.0px;
|
||||
-fx-background-radius: 4px;
|
||||
-fx-padding: 24px 24px 16px 24px;
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -569,6 +569,7 @@
|
||||
}
|
||||
|
||||
.task-executor-dialog-layout {
|
||||
-fx-background-radius: 4px;
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -912,28 +913,22 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
.jfx-progress-bar > .track {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .bar,
|
||||
.jfx-progress-bar:indeterminate > .bar{
|
||||
.jfx-progress-bar > .bar {
|
||||
-fx-background-color: -monet-primary;
|
||||
-fx-padding: 1.5;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .secondary-bar,
|
||||
.jfx-progress-bar:indeterminate > .secondary-bar {
|
||||
-fx-background-color: -monet-secondary;
|
||||
-fx-padding: 2;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .track,
|
||||
.jfx-progress-bar > .bar {
|
||||
-fx-background-radius: 0;
|
||||
-fx-background-radius: 2;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Textfield *
|
||||
@@ -1170,7 +1165,7 @@
|
||||
-fx-background-insets: 0px;
|
||||
|
||||
-jfx-toggle-color: -monet-primary;
|
||||
-jfx-toggle-line-color: -monet-primary-container;
|
||||
-jfx-toggle-line-color: -monet-secondary-container;
|
||||
-jfx-untoggle-color: -monet-outline;
|
||||
-jfx-untoggle-line-color: -monet-surface-container-highest;
|
||||
-jfx-size: 10;
|
||||
@@ -1456,20 +1451,8 @@
|
||||
-fx-stroke: -monet-primary-container;
|
||||
}
|
||||
|
||||
.jfx-spinner:determinate .percentage {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.first-spinner {
|
||||
-jfx-radius: 20;
|
||||
}
|
||||
|
||||
.first-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.small-spinner {
|
||||
-jfx-radius: 10;
|
||||
-jfx-radius: 9;
|
||||
}
|
||||
|
||||
.small-spinner .arc {
|
||||
@@ -1477,59 +1460,12 @@
|
||||
}
|
||||
|
||||
.small-spinner-pane .jfx-spinner {
|
||||
-jfx-radius: 10;
|
||||
-jfx-radius: 9;
|
||||
}
|
||||
|
||||
.small-spinner-pane .jfx-spinner .arc {
|
||||
-fx-stroke-width: 3.0;
|
||||
}
|
||||
|
||||
.second-spinner {
|
||||
-jfx-radius: 30;
|
||||
}
|
||||
|
||||
.second-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.third-spinner {
|
||||
-jfx-radius: 40;
|
||||
}
|
||||
|
||||
.third-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.fourth-spinner {
|
||||
-jfx-radius: 50;
|
||||
}
|
||||
|
||||
.fourth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.fifth-spinner {
|
||||
-jfx-radius: 60;
|
||||
}
|
||||
|
||||
.fifth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.sixth-spinner {
|
||||
-jfx-radius: 70;
|
||||
}
|
||||
|
||||
.sixth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.seventh-spinner {
|
||||
-jfx-radius: 80;
|
||||
}
|
||||
|
||||
.seventh-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
.spinner-pane .spinner-pane-content {
|
||||
-fx-padding: 10px;
|
||||
-fx-spacing: 10px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -1634,6 +1570,21 @@
|
||||
-fx-background-color: -monet-on-surface;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-actions {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-padding: 0.0 10.0 5.0 0.0;
|
||||
-fx-alignment: center-right;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-accept {
|
||||
-fx-padding: 0.7em 0.8em;
|
||||
}
|
||||
|
||||
.custom-color-dialog .jfx-color-dialog-cancel {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
-fx-padding: 0.7em 0.8em;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Table View *
|
||||
@@ -1941,6 +1892,10 @@
|
||||
-fx-padding: 10 16 10 16;
|
||||
}
|
||||
|
||||
.line-component .subtitle-label {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.line-component.no-padding .line-component-container {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
@@ -1970,14 +1925,26 @@
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .title-label {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .subtitle-label {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .title-label {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .subtitle-label {
|
||||
-fx-text-fill: -monet-primary;
|
||||
-fx-text-fill: -monet-primary-container;
|
||||
}
|
||||
|
||||
.line-toggle-button .jfx-toggle-button {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.update-pane:active .subtitle-label {
|
||||
-fx-text-fill: -monet-tertiary;
|
||||
}
|
||||
@@ -167,6 +167,9 @@ account.skin.upload.failed=Failed to upload skin.
|
||||
account.skin.invalid_skin=Invalid skin file.
|
||||
account.username=Username
|
||||
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
archive.file.name=File Name
|
||||
@@ -334,6 +337,10 @@ curse.category.4550=Quests
|
||||
curse.category.4555=World Gen
|
||||
curse.category.4552=Scripts
|
||||
|
||||
curse.category.6553=Realistic
|
||||
curse.category.6554=Fantasy
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=Date Created
|
||||
curse.sort.last_updated=Last Updated
|
||||
@@ -393,7 +400,7 @@ extension.ps1=Windows PowerShell Script
|
||||
extension.sh=Shell Script
|
||||
extension.command=macOS Shell Script
|
||||
|
||||
extension.datapack=Datapack Archive
|
||||
extension.datapack=Data Pack Archive
|
||||
extension.mod=Mod File
|
||||
extension.modloader.installer=Mod Loader Installer
|
||||
extension.resourcepack=Resource Pack Archive
|
||||
@@ -439,7 +446,7 @@ You can continue to keep the old version of Java.\
|
||||
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.download_link=Download Java %d
|
||||
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
|
||||
\n\
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
@@ -451,6 +458,8 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided
|
||||
\n\
|
||||
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
|
||||
Click the link to navigate to the Microsoft Store and install the compatibility pack.
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.
|
||||
|
||||
file=File
|
||||
|
||||
@@ -698,7 +707,7 @@ game.version=Game Instance
|
||||
|
||||
help=Help
|
||||
help.doc=Hello Minecraft! Launcher Documentation
|
||||
help.detail=For datapack and modpack makers.
|
||||
help.detail=For data pack and modpack makers.
|
||||
|
||||
input.email=The username must be an email address.
|
||||
input.number=The input must be numbers.
|
||||
@@ -794,7 +803,7 @@ launch.advice=%s Do you still want to continue to launch?
|
||||
launch.advice.multi=The following problems were detected:\n\n%s\n\nThese problems may prevent you from launching the game or affect gaming experience.\nDo you still want to continue to launch?
|
||||
launch.advice.java.auto=The current Java version is not compatible with the instance.\n\nClick "Yes" to automatically choose the most compatible Java version. Or, you can navigate to "Global/Instance-specific Settings → Java" to choose one yourself.
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 and previous versions require Java 7 or earlier.
|
||||
launch.advice.cleanroom=Cleanroom can only be run on Java 21 or later. Please use Java 21 or later versions.
|
||||
launch.advice.cleanroom=Cleanroom %2$s can only be run on Java %1$s or later. Please use Java %1$s or later versions.
|
||||
launch.advice.corrected=We have resolved the Java problem. If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.uncorrected=If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.different_platform=The 64-bit Java version is recommended for your device, but you have installed a 32-bit one.
|
||||
@@ -804,6 +813,8 @@ launch.advice.forge37_0_60=Forge versions prior to 37.0.60 are not compatible wi
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or later. Please use Java 8 or later versions.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions prior to 1.8.0_51. Please install the latest Java 8 version.
|
||||
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later. Please use Java 8 instead.
|
||||
launch.advice.lwjgl_3_4_1=The current game version has a known issue that may cause stuttering in the game.\nHMCL can automatically optimize the game performance (using a Java Agent).\nDo you allow HMCL to automatically optimize the game?\nIf you encounter issues, you can disable the "Allow HMCL to modify the game" option in "Settings → General".
|
||||
launch.advice.lwjgl_3_4_1.title=Allow HMCL to automatically optimize the game?
|
||||
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %1$s to launch Minecraft %2$s.
|
||||
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge.
|
||||
launch.advice.newer_java=You are using an older Java version to launch the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash.
|
||||
@@ -840,6 +851,8 @@ launcher.agreement=ToS and EULA
|
||||
launcher.agreement.accept=Accept
|
||||
launcher.agreement.decline=Decline
|
||||
launcher.agreement.hint=You must agree to the EULA to use this software.
|
||||
launcher.april_fools.switch_lzh=HMCL Launcher now supports Classical Chinese, do you want to switch to Classical Chinese mode?\nYou can switch back to English in "置設 → 貫用 → 文".
|
||||
launcher.april_fools.switch_lzh.confirm=Confirm switching to Classical Chinese mode?\nClick "OK" to exit HMCL and restart it in Classical Chinese mode; click "Cancel" to keep the current language and enter HMCL homepage.
|
||||
launcher.background=Background Image
|
||||
launcher.background.choose=Choose background image
|
||||
launcher.background.classic=Classic
|
||||
@@ -984,7 +997,7 @@ modrinth.category.colored-lighting=Colored Lighting
|
||||
modrinth.category.combat=Combat
|
||||
modrinth.category.core-shaders=Core Shaders
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.datapack=Datapack
|
||||
modrinth.category.datapack=Data Pack
|
||||
modrinth.category.decoration=Decoration
|
||||
modrinth.category.economy=Economy
|
||||
modrinth.category.entities=Entities
|
||||
@@ -1127,11 +1140,11 @@ nbt.open.failed=Failed to open file
|
||||
nbt.save.failed=Failed to save file
|
||||
nbt.title=View File - %s
|
||||
|
||||
datapack=Datapacks
|
||||
datapack=Data Packs
|
||||
datapack.add=Add
|
||||
datapack.add.title=Choose datapack archive you want to add
|
||||
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
|
||||
datapack.title=World [%s] - Datapacks
|
||||
datapack.add.title=Choose data pack archive you want to add
|
||||
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
|
||||
datapack.title=World [%s] - Data Packs
|
||||
|
||||
web.failed=Failed to load page
|
||||
web.open_in_browser=Do you want to open this address in a browser:\n%s
|
||||
@@ -1161,7 +1174,7 @@ world.duplicate.failed.empty_name=Name cannot be empty
|
||||
world.duplicate.failed.invalid_name=Name contains invalid characters
|
||||
world.duplicate.failed=Failed to duplicate the world
|
||||
world.duplicate.success.toast=Successfully duplicated the world
|
||||
world.datapack=Datapacks
|
||||
world.datapack=Data Packs
|
||||
world.datetime=Last played on %s
|
||||
world.delete=Delete the World
|
||||
world.delete.failed=Failed to delete world.\n%s
|
||||
@@ -1375,8 +1388,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibilit
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
settings.advanced.use_native_glfw=[Linux/FreeBSD Only] Use System GLFW
|
||||
settings.advanced.use_native_openal=[Linux/FreeBSD Only] Use System OpenAL
|
||||
settings.advanced.use_native_glfw=Use System GLFW
|
||||
settings.advanced.use_native_openal=Use System OpenAL
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD Only
|
||||
settings.advanced.workaround=Workaround
|
||||
settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options.
|
||||
settings.advanced.wrapper_launcher=Wrapper Command
|
||||
@@ -1410,6 +1424,8 @@ settings.game.working_directory.hint=Enable the "Isolated" option in "Working Di
|
||||
settings.icon=Icon
|
||||
|
||||
settings.launcher=Launcher Settings
|
||||
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
|
||||
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
|
||||
settings.launcher.appearance=Appearance
|
||||
settings.launcher.brightness=Theme Mode
|
||||
settings.launcher.brightness.auto=Follow System Settings
|
||||
@@ -1417,6 +1433,7 @@ settings.launcher.brightness.dark=Dark Mode
|
||||
settings.launcher.brightness.light=Light Mode
|
||||
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
|
||||
settings.launcher.debug=Debug
|
||||
settings.launcher.disable_april_fools=Do not enable April Fools features
|
||||
settings.launcher.disable_auto_game_options=Do not switch game language
|
||||
settings.launcher.download=Download
|
||||
settings.launcher.download.threads=Threads
|
||||
@@ -1424,6 +1441,7 @@ settings.launcher.download.threads.auto=Automatically Determine
|
||||
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.download_source.auto=Automatically Choose Download Sources
|
||||
settings.launcher.default_addon_source=Default Addon Source
|
||||
settings.launcher.enable_game_list=Show instance list in homepage
|
||||
settings.launcher.font=Font
|
||||
settings.launcher.font.anti_aliasing=Anti-aliasing
|
||||
@@ -1572,7 +1590,7 @@ update=Update
|
||||
update.accept=Update
|
||||
update.changelog=Changelog
|
||||
update.channel.dev=Beta
|
||||
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Release channel builds.\n\
|
||||
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
|
||||
\n\
|
||||
If you encounter any bugs or problems, please submit feedback via the channels provided on the <a href="hmcl://settings/feedback">Feedback</a> page.\n\
|
||||
\n\
|
||||
@@ -1585,14 +1603,17 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
|
||||
\n\
|
||||
Follow <a href="https://space.bilibili.com/1445341">@huanghongxun</a> on Bilibili to stay up to date on important HMCL news, or <a href="https://space.bilibili.com/20314891">@Glavo</a> to learn about HMCL development progress.
|
||||
update.channel.nightly.title=Nightly Channel Notice
|
||||
update.channel.stable=Release
|
||||
update.channel.stable=Stable
|
||||
update.checking=Checking for updates
|
||||
update.disable_auto_show_update_dialog=Do not show update dialog automatically
|
||||
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
|
||||
update.failed=Failed to update
|
||||
update.found=Update Available!
|
||||
update.newest_version=Latest version: %s
|
||||
update.bubble.title=Update Available: %s
|
||||
update.bubble.subtitle=Click here to update
|
||||
update.note=Beta and Nightly channels may have more features or fixes, but they also come with more potential problems.
|
||||
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
|
||||
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
|
||||
update.latest=This is the latest version
|
||||
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
|
||||
update.tooltip=Update
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1279,8 +1279,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibi
|
||||
settings.advanced.server_ip=Dirección del servidor
|
||||
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
|
||||
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
|
||||
settings.advanced.use_native_glfw=[Sólo Linux/FreeBSD] Utilizar GLFW nativo
|
||||
settings.advanced.use_native_openal=[Sólo Linux/FreeBSD] Utilizar OpenAL nativo
|
||||
settings.advanced.use_native_glfw=Utilizar GLFW nativo
|
||||
settings.advanced.use_native_openal=Utilizar OpenAL nativo
|
||||
settings.advanced.linux_freebsd_only=Sólo Linux/FreeBSD
|
||||
settings.advanced.workaround=Métodos alternativos
|
||||
settings.advanced.workaround.warning=Éstas opciones están pensadas sólo para usuarios expertos. Jugar con estas opciones puede romper el juego. A menos que sepas lo que estás haciendo, por favor no modifiques estas opciones.
|
||||
settings.advanced.wrapper_launcher=Comando Wrapper
|
||||
@@ -1401,7 +1402,6 @@ update.found=¡Actualización disponible!
|
||||
update.newest_version=Última versión: %s
|
||||
update.bubble.title=Actualización disponible: %s
|
||||
update.bubble.subtitle=Haga clic aquí para actualizar
|
||||
update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden tener más funciones o correcciones, pero también tienen más problemas potenciales.
|
||||
update.latest=Esta es la última versión.
|
||||
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
|
||||
update.tooltip=Actualización
|
||||
|
||||
@@ -802,8 +802,9 @@ settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
|
||||
settings.advanced.use_native_glfw=[Linux/FreeBSDのみ]システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=[Linux/FreeBSDのみ]システムOpenALを使用する
|
||||
settings.advanced.use_native_glfw=システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=システムOpenALを使用する
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSDのみ
|
||||
settings.advanced.workaround=デバッグ用オプション
|
||||
settings.advanced.workaround.warning=デバッグオプションはプロフェッショナルのみ使用可能です。デバッグオプションにより、ゲームが起動しない場合があります。これらのオプションは、ご自分が何をしているのかが分からない限り、変更しないでください。
|
||||
settings.advanced.wrapper_launcher=パッキングオーダー
|
||||
@@ -911,7 +912,6 @@ update.found=アップデートが利用可能です!
|
||||
update.newest_version=最新バージョン:%s
|
||||
update.bubble.title=利用可能な更新:%s
|
||||
update.bubble.subtitle=更新するにはここをクリックしてください
|
||||
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
|
||||
update.latest=これは最新バージョンです。
|
||||
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
|
||||
update.tooltip=更新
|
||||
|
||||
@@ -579,6 +579,8 @@ launch.advice.forge37_0_60=鍛三七點〇點五九與舊者弗兼爪哇十七
|
||||
launch.advice.java8_1_13=礦藝一點一三以晉但行於爪哇八與晉者。宜行以爪哇八與晉者。
|
||||
launch.advice.java8_51_1_13=爪哇之前一點八點〇之五一者或崩礦藝一點一三。宜新爪哇於一點八點〇之五一以晉,然後復啟。
|
||||
launch.advice.java9=礦藝之前一點一三而有改囊者弗見兼於爪哇九與晉者。宜啟以爪哇八。
|
||||
launch.advice.lwjgl_3_4_1=戲之是版有已知之謬,行戲之時或致頓滯。\nHMCL 可假爪哇 Agent 自優其戲,以弭是患。君其許 HMCL 自優其戲乎?\n誠有謬,可至於「置設 → 貫用」,廢「許改戲」之項。
|
||||
launch.advice.lwjgl_3_4_1.title=君其許 HMCL 自優其戲乎?
|
||||
launch.advice.modded_java=改囊或弗見兼於爪哇之新者。宜以爪哇 %s 啟礦藝 %s。
|
||||
launch.advice.modlauncher8=鍛版之所行弗兼於爪哇之所行。宜新鍛。
|
||||
launch.advice.newer_java=余識君之啟戲以舊爪哇也,蓋改囊或是以崩戲也。宜新爪哇於八,然後復啟。
|
||||
@@ -975,7 +977,10 @@ repositories.chooser=闕 JavaFX。HMCL 須之以行。\n擊「善」而引 JavaF
|
||||
repositories.chooser.title=擇 JavaFX 之源
|
||||
|
||||
resourcepack=資囊
|
||||
resourcepack.add=增資囊
|
||||
resourcepack.download=引資囊
|
||||
resourcepack.download.title=引資囊 - %1s
|
||||
resourcepack.manage=司資囊
|
||||
|
||||
reveal.in_file_manager=覽於理案臺
|
||||
|
||||
@@ -1090,8 +1095,9 @@ settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
|
||||
settings.advanced.server_ip=伺服器之址
|
||||
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
|
||||
settings.advanced.unsupported_system_options=不逮今之械網所選
|
||||
settings.advanced.use_native_glfw=[惟磷若/FreeBSD] 用 GLFW
|
||||
settings.advanced.use_native_openal=[惟磷若/FreeBSD] 用開聲庫
|
||||
settings.advanced.use_native_glfw=用 GLFW
|
||||
settings.advanced.use_native_openal=用開聲庫
|
||||
settings.advanced.linux_freebsd_only=惟磷若/FreeBSD
|
||||
settings.advanced.workaround=勘誤置設
|
||||
settings.advanced.workaround.warning=勘誤置設者,蓋專者之所行也。苟妄改之,戲或弗啟。微其之知,無復改焉!
|
||||
settings.advanced.wrapper_launcher=包裝之令
|
||||
@@ -1123,6 +1129,8 @@ settings.game.working_directory.hint=夫于「運徑」擇「例獨」者,可
|
||||
settings.icon=戲徵
|
||||
|
||||
settings.launcher=啟者置設
|
||||
settings.launcher.allow_auto_agent=許改戲
|
||||
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
|
||||
settings.launcher.appearance=外觀
|
||||
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
|
||||
settings.launcher.debug=勘誤
|
||||
@@ -1140,7 +1148,7 @@ settings.launcher.font.anti_aliasing.auto=自調
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.general=貫用
|
||||
settings.launcher.language=文 (複啟而效)
|
||||
settings.launcher.language=文
|
||||
settings.launcher.launcher_log.export=錄出啟者之誌
|
||||
settings.launcher.launcher_log.export.failed=錄誌出而未成
|
||||
settings.launcher.launcher_log.export.success=誌既存於「%s」
|
||||
@@ -1193,6 +1201,8 @@ sponsor.hmcl=Hello Minecraft! Launcher,乃一免費,自由,開源之礦藝
|
||||
system.architecture=大構
|
||||
system.operating_system=械網
|
||||
|
||||
terracotta=眾戲
|
||||
|
||||
unofficial.hint=子方用非官築之 HMCL。余無能證其安,請君自辨。
|
||||
|
||||
update=啟者迭更
|
||||
@@ -1210,15 +1220,18 @@ update.channel.nightly.hint=君所用之 HMCL 乃預版也。預版之更迭常
|
||||
update.channel.nightly.title=預版告
|
||||
update.channel.stable=定版
|
||||
update.checking=甫察其新
|
||||
update.disable_auto_show_update_dialog=弗自顯迭更窗
|
||||
update.disable_auto_show_update_dialog.subtitle=啟此,HMCL 將弗自顯迭更窗。
|
||||
update.failed=迭更未成
|
||||
update.found=尋得新者
|
||||
update.newest_version=至新:%s
|
||||
update.bubble.title=尋得更新:%s
|
||||
update.bubble.subtitle=是擊以晉
|
||||
update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
|
||||
update.latest=當版至新
|
||||
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
|
||||
update.tooltip=迭更
|
||||
update.preview=先覽 HMCL 之版
|
||||
update.preview.subtitle=啟此,君可先得 HMCL 之新,試之於未行。
|
||||
|
||||
version=戲
|
||||
version.name=戲例之名
|
||||
|
||||
@@ -1270,8 +1270,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Лучшая производите
|
||||
settings.advanced.server_ip=Адрес сервера
|
||||
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
|
||||
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
|
||||
settings.advanced.use_native_glfw=[Только для Linux/FreeBSD] Использовать системный GLFW
|
||||
settings.advanced.use_native_openal=[Только для Linux/FreeBSD] Использовать системный OpenAL
|
||||
settings.advanced.use_native_glfw=Использовать системный GLFW
|
||||
settings.advanced.use_native_openal=Использовать системный OpenAL
|
||||
settings.advanced.linux_freebsd_only=Только для Linux/FreeBSD
|
||||
settings.advanced.workaround=Обходные пути
|
||||
settings.advanced.workaround.warning=Варианты обхода предназначены только для опытных пользователей. Изменение этих параметров может привести к вылету игры. Если не знаете, что делаете, то не изменяйте эти параметры.
|
||||
settings.advanced.wrapper_launcher=Команда-оболочка
|
||||
@@ -1391,7 +1392,6 @@ update.found=Доступно обновление!
|
||||
update.newest_version=Последняя версия: %s
|
||||
update.bubble.title=Доступно обновление: %s
|
||||
update.bubble.subtitle=Кликните здесь для обновления
|
||||
update.note=Предупреждение: Бета-версии могут иметь больше функциональных возможностей и исправлений ошибок, но также и больше возможных ошибок.
|
||||
update.latest=Это последняя версия.
|
||||
update.no_browser=Не удалось открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления.
|
||||
update.tooltip=Обновить
|
||||
|
||||
@@ -1217,8 +1217,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктив
|
||||
settings.advanced.server_ip=Адреса сервера
|
||||
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
settings.advanced.use_native_glfw=[Лише Linux/FreeBSD] Використовувати системний GLFW
|
||||
settings.advanced.use_native_openal=[Лише Linux/FreeBSD] Використовувати системний OpenAL
|
||||
settings.advanced.use_native_glfw=Використовувати системний GLFW
|
||||
settings.advanced.use_native_openal=Використовувати системний OpenAL
|
||||
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
|
||||
settings.advanced.workaround=Обхідні шляхи
|
||||
settings.advanced.workaround.warning=Опції обхідних шляхів призначені лише для досвідчених користувачів. Налаштування цих опцій може призвести до збою гри. Якщо ви не знаєте, що робите, не редагуйте ці опції.
|
||||
settings.advanced.wrapper_launcher=Команда обгортки
|
||||
@@ -1342,7 +1343,6 @@ update.found=Доступне оновлення!
|
||||
update.newest_version=Остання версія: %s
|
||||
update.bubble.title=Доступне оновлення: %s
|
||||
update.bubble.subtitle=Натисніть тут, щоб оновити
|
||||
update.note=Канали Бета та Нічний можуть мати більше функцій або виправлень, але вони також мають більше потенційних проблем.
|
||||
update.latest=Це остання версія
|
||||
update.no_browser=Не вдається відкрити в системному браузері. Але ми скопіювали посилання до вашого буфера обміну, і ви можете відкрити його вручну.
|
||||
update.tooltip=Оновити
|
||||
|
||||
@@ -164,6 +164,9 @@ account.skin.upload.failed=外觀上傳失敗
|
||||
account.skin.invalid_skin=無法識別的外觀檔案
|
||||
account.username=使用者名稱
|
||||
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
archive.file.name=檔案名稱
|
||||
@@ -330,6 +333,10 @@ curse.category.4550=任務
|
||||
curse.category.4555=世界生成
|
||||
curse.category.4552=指令碼
|
||||
|
||||
curse.category.6553=寫實
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=建立日期
|
||||
curse.sort.last_updated=最近更新
|
||||
@@ -408,12 +415,14 @@ fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執
|
||||
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能執行,請安裝最新版本的 Java 以確保 HMCL 能夠完成升級。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
fatal.deprecated_java_version.download_link=下載 Java %d
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
|
||||
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
|
||||
|
||||
file=檔案
|
||||
|
||||
@@ -598,7 +607,7 @@ launch.advice=%s 是否繼續啟動?
|
||||
launch.advice.multi=檢測到以下問題:\n\n%s\n\n這些問題可能導致遊戲無法正常啟動或影響遊戲體驗,是否繼續啟動?
|
||||
launch.advice.java.auto=目前選取的 Java 版本不滿足遊戲要求,是否自動選取合適的 Java 版本?\n或者你可以到「(全域/實例特定) 遊戲設定 → 遊戲 Java」中選取一個合適的 Java 版本。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
|
||||
launch.advice.cleanroom=Cleanroom 僅支援 Java 21 或更高版本。請使用 Java 21 或最新版本。
|
||||
launch.advice.cleanroom=Cleanroom %2$s 只能在 Java %1$s 或更高版本上運行。請使用 Java %1$s 或最新版本。
|
||||
launch.advice.corrected=我們已經修正了 Java 版本問題。如果你確實希望使用你自訂的 Java,你可以在「(全域/實例特定) 遊戲設定 → 進階設定」中往下滑,開啟「不檢查 Java 虛擬機與遊戲的相容性」。
|
||||
launch.advice.uncorrected=如果你確實希望使用你自訂的 Java,你可以在「(全域/實例特定) 遊戲設定 → 進階設定」中往下滑,開啟「不檢查 Java 虛擬機與遊戲的相容性」。
|
||||
launch.advice.different_platform=你正在使用 32 位元 Java 啟動遊戲。建議更換至 64 位元 Java。
|
||||
@@ -608,6 +617,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本與 Java 17 不相容。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 及更高版本僅支援 Java 8 或更高版本。請使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議你到 https://java.com 安裝最新版的 Java 8。
|
||||
launch.advice.java9=低於 (包含) 1.13 的有安裝模組的 Minecraft 版本不支援 Java 9 或更高版本。請使用 Java 8。
|
||||
launch.advice.lwjgl_3_4_1=當前遊戲版本存在已知問題,可能在遊戲中發生卡頓。\nHMCL 可以 (透過 Java Agent) 自動最佳化遊戲解決此問題。是否允許 HMCL 自動最佳化遊戲?\n如遇到問題,你可以在「設定 → 一般」中關閉「允許 HMCL 修改遊戲」選項。
|
||||
launch.advice.lwjgl_3_4_1.title=是否允許 HMCL 自動最佳化遊戲?
|
||||
launch.advice.modded_java=部分模組可能與高版本 Java 不相容。建議使用 Java %s 啟動 Minecraft %s。
|
||||
launch.advice.modlauncher8=你所使用的 Forge 版本與目前使用的 Java 不相容。請更新 Forge。
|
||||
launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分模組引發遊戲崩潰。建議更新至 Java 8 後再次啟動。
|
||||
@@ -644,6 +655,8 @@ launcher.agreement=使用者協議與免責宣告
|
||||
launcher.agreement.accept=同意
|
||||
launcher.agreement.decline=拒絕
|
||||
launcher.agreement.hint=同意本軟體的使用者協議與免責宣告以使用本軟體。
|
||||
launcher.april_fools.switch_lzh=HMCL 啟動器現已支援文言文,我們誠邀你參與體驗。是否要將介面語言切換至中文 (文言)?\n在切換語言後,你可以在「置設 → 貫用 → 文」中將語言切換回中文 (繁體)。
|
||||
launcher.april_fools.switch_lzh.confirm=確認切換語言?\n點擊「確定」後 HMCL 將自動重新啟動並將介面語言切換至中文 (文言);點擊「取消」將繼續使用中文 (繁體) 進入 HMCL 主頁。
|
||||
launcher.background=背景圖片
|
||||
launcher.background.choose=選取背景圖片
|
||||
launcher.background.classic=經典
|
||||
@@ -1162,8 +1175,9 @@ settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
|
||||
settings.advanced.server_ip=伺服器位址
|
||||
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.unsupported_system_options=不適用於目前系統的選項
|
||||
settings.advanced.use_native_glfw=[僅限 Linux/FreeBSD] 使用系統 GLFW
|
||||
settings.advanced.use_native_openal=[僅限 Linux/FreeBSD] 使用系統 OpenAL
|
||||
settings.advanced.use_native_glfw=使用系統 GLFW
|
||||
settings.advanced.use_native_openal=使用系統 OpenAL
|
||||
settings.advanced.linux_freebsd_only=僅限 Linux/FreeBSD
|
||||
settings.advanced.workaround=除錯選項
|
||||
settings.advanced.workaround.warning=除錯選項僅提供給專業玩家使用。修改除錯選項可能會導致遊戲無法啟動。除非你知道你在做什麼,否則請不要修改這些選項。
|
||||
settings.advanced.wrapper_launcher=前置指令
|
||||
@@ -1195,6 +1209,8 @@ settings.game.working_directory.hint=在「執行路徑」選項中選取「各
|
||||
settings.icon=遊戲圖示
|
||||
|
||||
settings.launcher=啟動器設定
|
||||
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
|
||||
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
|
||||
settings.launcher.appearance=外觀
|
||||
settings.launcher.brightness=主題模式
|
||||
settings.launcher.brightness.auto=跟隨系統設定
|
||||
@@ -1202,6 +1218,7 @@ settings.launcher.brightness.dark=深色模式
|
||||
settings.launcher.brightness.light=淺色模式
|
||||
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。
|
||||
settings.launcher.debug=除錯
|
||||
settings.launcher.disable_april_fools=不啟用愚人節功能
|
||||
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
|
||||
settings.launcher.download=下載
|
||||
settings.launcher.download.threads=執行緒數
|
||||
@@ -1209,6 +1226,7 @@ settings.launcher.download.threads.auto=自動選取執行緒數
|
||||
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.launcher.download_source.auto=自動選取下載來源
|
||||
settings.launcher.default_addon_source=遊戲內容預設下載來源
|
||||
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
|
||||
settings.launcher.font=字體
|
||||
settings.launcher.font.anti_aliasing=反鋸齒
|
||||
@@ -1368,12 +1386,15 @@ update.channel.nightly.hint=你正在使用 HMCL 預覽版。預覽版更新較
|
||||
update.channel.nightly.title=預覽版提示
|
||||
update.channel.stable=穩定版
|
||||
update.checking=正在檢查更新
|
||||
update.disable_auto_show_update_dialog=不自動顯示更新彈窗
|
||||
update.disable_auto_show_update_dialog.subtitle=啟用此選項,HMCL 將不會自動彈出更新彈窗。
|
||||
update.failed=更新失敗
|
||||
update.found=發現到更新
|
||||
update.newest_version=最新版本為:%s
|
||||
update.bubble.title=發現更新:%s
|
||||
update.bubble.subtitle=點選此處進行升級
|
||||
update.note=開發版與預覽版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
|
||||
update.note.stable=適合優先追求軟體穩定性的使用者使用。\n新功能在經過充分測試後才會被添加到穩定版中。
|
||||
update.note.dev=適合希望優先體驗新功能的使用者使用。\n開發版會包含最新功能和錯誤修復,但未經充分測試,可能會存在更多問題。
|
||||
update.latest=目前版本為最新版本
|
||||
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
|
||||
update.tooltip=更新
|
||||
|
||||
@@ -166,6 +166,9 @@ account.skin.upload.failed=皮肤上传失败
|
||||
account.skin.invalid_skin=无法识别的皮肤文件
|
||||
account.username=用户名
|
||||
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
archive.file.name=文件名
|
||||
@@ -332,6 +335,10 @@ curse.category.4550=任务
|
||||
curse.category.4555=世界生成
|
||||
curse.category.4552=脚本
|
||||
|
||||
curse.category.6553=写实
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=创建日期
|
||||
curse.sort.last_updated=最近更新
|
||||
@@ -411,13 +418,15 @@ fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运
|
||||
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能运行,请安装最新版本的 Java 以确保 HMCL 能够完成升级。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。
|
||||
fatal.deprecated_java_version.download_link=下载 Java %d
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.illegal_char=由于你的运行路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
|
||||
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
|
||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
|
||||
|
||||
file=文件
|
||||
|
||||
@@ -603,7 +612,7 @@ launch.advice=%s 是否继续启动?
|
||||
launch.advice.multi=检测到以下问题:\n\n%s\n\n这些问题可能导致游戏无法正常启动或影响游戏体验,是否继续启动?\n你可以点击右上角帮助按钮进行求助。
|
||||
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
|
||||
launch.advice.cleanroom=Cleanroom 只能在 Java 21 或更高版本上运行。请使用 Java 21 或最新版本。
|
||||
launch.advice.cleanroom=Cleanroom %2$s 只能在 Java %1$s 或更高版本上运行。请使用 Java %1$s 或最新版本。
|
||||
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
|
||||
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
|
||||
launch.advice.different_platform=你正在使用 32 位 Java 启动游戏。建议更换至 64 位 Java。
|
||||
@@ -613,6 +622,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本与 Java 17 不兼容。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 及更高版本只能在 Java 8 或更高版本上运行。请使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议更新 Java 至 1.8.0_51 或更高版本后再次启动。
|
||||
launch.advice.java9=低于 1.13 的有安装模组的 Minecraft 版本与 Java 9 或更高版本不兼容。请使用 Java 8。
|
||||
launch.advice.lwjgl_3_4_1=当前游戏版本存在已知问题,可能在游戏中发生卡顿。\nHMCL 可以 (通过 Java Agent) 自动优化游戏解决此问题。是否允许 HMCL 自动优化游戏?\n如遇到问题,你可以在“设置 → 通用”中关闭“允许 HMCL 修改游戏”选项。
|
||||
launch.advice.lwjgl_3_4_1.title=是否允许 HMCL 自动优化游戏?
|
||||
launch.advice.modded_java=部分模组可能与高版本 Java 不兼容。建议使用 Java %s 启动 Minecraft %s。
|
||||
launch.advice.modlauncher8=你所使用的 Forge 版本与当前使用的 Java 不兼容。请更新 Forge。
|
||||
launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃。建议更新至 Java 8 后再次启动。
|
||||
@@ -649,6 +660,8 @@ launcher.agreement=用户协议与免责声明
|
||||
launcher.agreement.accept=同意
|
||||
launcher.agreement.decline=拒绝
|
||||
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件。
|
||||
launcher.april_fools.switch_lzh=HMCL 启动器现已支持文言文,我们诚邀你参与体验。是否要将界面语言切换至中文 (文言)?\n在切换语言后,你可以在“置設 → 貫用 → 文”中将语言切换回中文 (简体)。
|
||||
launcher.april_fools.switch_lzh.confirm=确认切换语言?\n点击“确定”后 HMCL 将自动重启并将界面语言切换至中文 (文言);点击“取消”将继续使用中文 (简体) 进入 HMCL 主页。
|
||||
launcher.background=背景图片
|
||||
launcher.background.choose=选择背景图片
|
||||
launcher.background.classic=经典
|
||||
@@ -1167,8 +1180,9 @@ settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
|
||||
settings.advanced.server_ip=服务器地址
|
||||
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
|
||||
settings.advanced.unsupported_system_options=不适用于当前系统的选项
|
||||
settings.advanced.use_native_glfw=[仅 Linux/FreeBSD] 使用系统 GLFW
|
||||
settings.advanced.use_native_openal=[仅 Linux/FreeBSD] 使用系统 OpenAL
|
||||
settings.advanced.use_native_glfw=使用系统 GLFW
|
||||
settings.advanced.use_native_openal=使用系统 OpenAL
|
||||
settings.advanced.linux_freebsd_only=仅 Linux/FreeBSD
|
||||
settings.advanced.workaround=调试选项
|
||||
settings.advanced.workaround.warning=调试选项仅提供给专业玩家使用。调试选项可能会导致游戏无法启动。除非你知道你在做什么,否则请不要修改这些选项!
|
||||
settings.advanced.wrapper_launcher=包装命令
|
||||
@@ -1200,6 +1214,8 @@ settings.game.working_directory.hint=在“版本隔离”中选择“各实例
|
||||
settings.icon=游戏图标
|
||||
|
||||
settings.launcher=启动器设置
|
||||
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
|
||||
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
|
||||
settings.launcher.appearance=外观
|
||||
settings.launcher.brightness=主题模式
|
||||
settings.launcher.brightness.auto=跟随系统设置
|
||||
@@ -1207,6 +1223,7 @@ settings.launcher.brightness.dark=深色模式
|
||||
settings.launcher.brightness.light=浅色模式
|
||||
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。
|
||||
settings.launcher.debug=调试
|
||||
settings.launcher.disable_april_fools=不启用愚人节功能
|
||||
settings.launcher.disable_auto_game_options=不自动切换游戏语言
|
||||
settings.launcher.download=下载
|
||||
settings.launcher.download.threads=线程数
|
||||
@@ -1214,6 +1231,7 @@ settings.launcher.download.threads.auto=自动选择线程数
|
||||
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.download_source.auto=自动选择下载源
|
||||
settings.launcher.default_addon_source=游戏内容默认下载源
|
||||
settings.launcher.enable_game_list=在主页内显示版本列表
|
||||
settings.launcher.font=字体
|
||||
settings.launcher.font.anti_aliasing=抗锯齿
|
||||
@@ -1373,12 +1391,15 @@ update.channel.nightly.hint=你正在使用 HMCL 预览版。预览版更新较
|
||||
update.channel.nightly.title=预览版提示
|
||||
update.channel.stable=稳定版
|
||||
update.checking=正在检查更新
|
||||
update.disable_auto_show_update_dialog=不自动显示更新弹窗
|
||||
update.disable_auto_show_update_dialog.subtitle=启用此选项,HMCL 将不会自动弹出更新弹窗。
|
||||
update.failed=更新失败
|
||||
update.found=发现更新
|
||||
update.newest_version=最新版本为:%s
|
||||
update.bubble.title=发现更新:%s
|
||||
update.bubble.subtitle=点击此处进行升级
|
||||
update.note=开发版与预览版包含更多的功能以及错误修复,但也可能会包含其他的问题。
|
||||
update.note.stable=适合优先追求软件稳定性的用户使用。\n新功能在经过充分测试后才会被添加到稳定版中。
|
||||
update.note.dev=适合希望优先体验新功能的用户使用。\n包含最新功能和错误修复,但未经充分测试,可能会存在更多问题。
|
||||
update.latest=当前版本为最新版本
|
||||
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
|
||||
update.tooltip=更新
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -354,7 +354,7 @@ all-the-mods-expert-remastered;355;;All The Mods Expert: Remastered;;
|
||||
all-the-mods-0;356;;All the Mods 0;;ATM0
|
||||
all-the-mods-fabric;357;;All the Mods Fabric;;ATMF
|
||||
the-pixelmon-modpack;358;;The Pixelmon Modpack;;
|
||||
better-mc-skyblock;359;;更好的我的世界空岛;Better MC Skyblock;BMCS
|
||||
better-mc-skyblock;359;;更好的我的世界空岛;Better Minecraft Skyblock;BMCS
|
||||
age-of-fate;360;;命运时代;Age of Fate;AOF
|
||||
minecraft-kingdoms;361;;MC Kingdoms;;
|
||||
;362;;幻想旅程;Imaginary Journey;
|
||||
@@ -364,7 +364,7 @@ valhelsia-4;365;;Valhelsia 4;;
|
||||
another-quality-modpack-3;366;;Another Quality Modpack 3;;
|
||||
nomifactory;367;;Nomifactory;;
|
||||
allusiveness;368;;Allusiveness;;
|
||||
the-way-of-reincarnation;370;;转生之路;The way of reincarnation;
|
||||
the-way-of-reincarnation;370;;转生之路;The Way of Reincarnation;
|
||||
sky-friends;371;;Sky Friends;;
|
||||
magic-pouch;372;;魔法包裹;Magic Pouch;
|
||||
magica-infinitum-iii;373;;Magica Infinitum III;;
|
||||
@@ -392,7 +392,7 @@ create-yourself;395;;Create Yourself;;
|
||||
starrynight-pray-stp;397;;星夜祈临;StarryNight Pray;StP
|
||||
rustic-waters-ii;398;;乡村水域2;Rustic Waters II;
|
||||
enigmatica6expert;399;;Enigmatica 6: Expert;;E6E
|
||||
celestial-journey;400;;Celestial Journey;;
|
||||
celestial-journey;400;;星空之旅;Celestial Journey;
|
||||
;401;;天空舞台;Skyblock Create Show;
|
||||
project-architect;402;;Project Architect;;
|
||||
greed-the-resource-3-factory;403;;Greed the Resource 3: Factory;;
|
||||
@@ -509,7 +509,7 @@ mechanical-mastery;516;;Mechanical Mastery;;MM
|
||||
escape-from-2020;517;;逃离2020;Escape From 2020;
|
||||
lunaria-expert;518;;Lunaria Expert;;
|
||||
nomi-ceu;519;;Nomifactory CEu;;Nomi-CEu
|
||||
;520;;乐事 · 起源;Delight · Origins;DO
|
||||
;520;;乐事 · 起源;Delight · Origins;DO
|
||||
tankoptimization;521;;坦坦优化;Tankoptimization;
|
||||
;522;;InfinityCraft;;IC
|
||||
pick-the-stars;523;;摘星;Pick The Stars;PTS
|
||||
@@ -612,7 +612,7 @@ eternal-night-modpacks;622;;永夜;Eternal Night;ETN
|
||||
all-the-mods-9;623;;All the Mods 9;;ATM9
|
||||
spoornpack;624;;Spoornpack;;
|
||||
;625;;起源RPG;;ORPG
|
||||
;626;;织彩钓月;Oriun Tagayasu;OTSU
|
||||
aruori;626;;织彩钓月;Aruori;
|
||||
;627;;冰河物语;;
|
||||
hommage;628;;Hommage;;
|
||||
days-to-craft;629;;Seven Days To Craft;;
|
||||
@@ -650,7 +650,7 @@ valhelsia-6;660;;Valhelsia 6;;
|
||||
all-the-mods-volcanoblock;661;;All the Mods: Volcano Block;;ATMVB
|
||||
gregtronix-revoluton;662;;Gregtronix Revolution;;GR
|
||||
;663;;空岛物语;Empty Island RPG;EIRPG
|
||||
prominence-fabric;664;;卓越1 [FABRIC];Prominence I;
|
||||
prominence-1-rpg;664;;卓越 I;Prominence I;
|
||||
multiskyblock-2;665;;模块化空岛2;MultiSkyBlock 2;MSB2
|
||||
;666;;The Minehattan Project;;
|
||||
;667;;万象天工;Millions of Heavens industries;MOHI
|
||||
@@ -701,7 +701,7 @@ create-perfect-world;711;;Create: Perfect World;;
|
||||
new-simple-mods;712;;New Simple Mods - Easy to Understand;;
|
||||
;713;;灾变录;Catastrophe Record;CR
|
||||
sky-greg;714;;Sky Greg;;
|
||||
deceasedcraft;715;;亡者世界;Deceased Craft;
|
||||
deceasedcraft;715;;亡者世界;DeceasedCraft - Urban Zombie Apocalypse;
|
||||
;716;;生欲:求生;Vivere The Zombie Apocalypse;VZA
|
||||
create-planetary;717;;Create Planetary;;
|
||||
master-mabaoguo;718;;Master mabaoguo;;
|
||||
@@ -754,7 +754,7 @@ modecube;765;;ModéCube;;
|
||||
electrictechnocraft;766;;ElectricTechnoCraft;;
|
||||
create-2-mekanism;767;;Create 2 Mekanism;;C2M
|
||||
randblock;768;;随机空岛;RandBlock;RD
|
||||
;769;;格雷科技休闲版;GregTech Leisure;GTL
|
||||
;769;;格雷科技休闲版;GregTech Leisure;GTL
|
||||
frozenopolis;770;;Frozenopolis;;
|
||||
infernal-rpg;771;;Infernal Origins;;
|
||||
;772;;天狼星;Sirius;
|
||||
@@ -866,9 +866,9 @@ create-vanilla-exploration;879;;Vanilla Create Exploration;;
|
||||
progress;880;;Progress: Create;;
|
||||
aeon-automata;881;;Aeon Automata;;AA
|
||||
;882;;被恐惧支配2:收容失效;Dominated by Fear 2 Containment Breach;DoF2
|
||||
;883;;剑拔弩张之时;;TC
|
||||
tensioncraft;883;;剑拔弩张之时;;TC
|
||||
project-architect-2;884;;Project Architect 2;;
|
||||
;885;;路网通途 / 接入铁路网络;Railway Network Access;RNA
|
||||
;885;;接入铁路网络:我的世界铁路;MTR Railway Network Access;RNA-MTR
|
||||
create-mekanized;886;;Create: Mekanized;;
|
||||
create-perfect-world-2;887;;Create: Perfect World 2;;
|
||||
create-stellar;888;;Create Stellar;;
|
||||
@@ -923,7 +923,7 @@ setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
|
||||
epic-journey-gtceu;937;;Epic Journey GTCEu;;
|
||||
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
|
||||
gregtech-skybound;939;;Gregtech: Skybound;;
|
||||
simple-tfc;940;;简单群峦;SimpleTFC;
|
||||
simple-tfc;940;;简单群峦;Simple TFC;
|
||||
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
|
||||
final-judgment-save-the-world;942;;Final Judgment:Save the world;;
|
||||
;943;;极端世界:重置;Extreme World Reset;EWR
|
||||
@@ -998,10 +998,10 @@ ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
|
||||
create-silence-and-prosperity;1014;;机械动力:寂静与繁荣;Create Silence and Prosperity;CSP
|
||||
mif;1015;;MI:Foundation;;MIF
|
||||
c2mse2;1016;;Create 2 Mekanism: Sky Edition 2;;C2MSE2
|
||||
;1017;;耕云钓月;Kumo O Tagayasu;KOTS
|
||||
kumo-o-tagayasu;1017;;耕云钓月;Kumo O Tagayasu;KOTS
|
||||
;1018;;无限战争4;Infinity War4;INF4
|
||||
;1019;;群峦传说:工业长路;;
|
||||
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
|
||||
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
|
||||
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
|
||||
create-star-valley;1022;;Create: Gears of History;;
|
||||
;1023;;阿尔法计划;AlphaPlan;AP
|
||||
@@ -1026,7 +1026,7 @@ compression;1041;;Compression;;
|
||||
compact-sky-easy;1042;;Compact Sky: Easy;;CSE
|
||||
;1043;;丰收与探索;Harvest and Exploration;
|
||||
;1044;;脆骨症:黯光;No Flesh Within Chest-DIM;NFWC-DIM
|
||||
;1045;;小雷的优化冒险;;
|
||||
;1045;;光陆冒险;;
|
||||
cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
|
||||
;1047;;Tinkerer's Quilt;;
|
||||
gregtech-expert;1048;;GregTech Expert;;
|
||||
@@ -1044,7 +1044,7 @@ ftb-oceanblock-2;1059;;FTB OceanBlock 2;;
|
||||
;1060;;伊甸空天2;Eden Aerospace 2;EAS2
|
||||
create-infinity-mechanism;1061;;机械动力:无限构件;Create:Infinity Mechanism;CIM
|
||||
beyond-depth;1062;;Beyond Depth;;
|
||||
;1063;;枪械交界地;TACZ Amid Crucible Zone;
|
||||
;1063;;枪械交界地-旧地狱篇;;
|
||||
;1064;;真幻世界;Realfantacy World;R.F.
|
||||
minenufactory;1065;;创世工厂;Minenufactory;MF
|
||||
craft-to-exile-2;1066;;放逐之路2;Craft to Exile 2 (VR Support);CTE2
|
||||
@@ -1058,7 +1058,7 @@ sky1234;1074;;sky1234;;
|
||||
star-technology;1075;;Star Technology;;
|
||||
;1076;;Hyper World;;HW
|
||||
;1077;;妖月的魔法金属整合包;yaoyaoDream's ManaMetalMod Modpack;YMM
|
||||
techcraft-severwinter;1078;;科技工艺:严冬;TechCraft: SevereWinter;TCSW
|
||||
forgottenwinter;1078;;遗忘严冬;ForgottenWinter;FTW
|
||||
randblock2-winterize;1079;;随机空岛 2:银装素裹;RandBlock2:Winterize;
|
||||
create-the-brass-concerto;1080;;机械动力:黄铜协奏曲;Create: The Brass Concerto;CBC
|
||||
explorers-eve;1081;;Explorer's Eve;;
|
||||
@@ -1071,7 +1071,6 @@ mierno;1087;;迷厄路;Mierno;
|
||||
;1088;;最后那些天;Those last day;
|
||||
;1089;;泠月狐基础整合;LunarFox;
|
||||
roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures and Dungeons 3;RAD3
|
||||
;1091;;羽舲界;Hanekmio Instance;HMI
|
||||
;1092;;玄理2:洗剪版;EnigTech 2 - Dishwasher's Cut;ET2DC
|
||||
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
|
||||
egged-up;1094;;Egged Up;;
|
||||
@@ -1110,15 +1109,15 @@ skillet-man;1123;;平底锅侠;Skillet Man;
|
||||
create-creative-and-beyond;1127;;机械动力:创造与超越;Create Creative and Beyond;CCB
|
||||
create-delight-remake;1128;;机械动力:齿轮盛宴;Create:Delight Remake;CDR
|
||||
ftb-skies-2;1129;;FTB Skies 2;;
|
||||
;1130;;星辰时代:科技魔法;Stellar Era: Technomagic;SETM
|
||||
;1130;;星熵:灵械;Stellar Era: Technomagic;SETM
|
||||
aircrashed-engineer;1131;;坠机工程师;Aircrashed Engineer;ACE
|
||||
project-tfcreate;1132;;Project: TFCreate;;
|
||||
;1133;;落幕曲;;
|
||||
;1133;;落幕曲;Closing Song;
|
||||
;1134;;超大陆:勇气;Supercontinent:Pluck;SUPL
|
||||
create-chronicles-the-endventure;1135;;Create Chronicles: The Endventure;;
|
||||
life-in-the-village-4;1136;;Life in the village 4;;LITV4
|
||||
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
|
||||
;1138;;奥术咖啡馆;;
|
||||
;1138;;奥术咖啡馆;Arcane Cofferia;
|
||||
unyielding;1139;;Unyielding;;
|
||||
;1140;;剑与王国;;SNK
|
||||
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
|
||||
@@ -1140,26 +1139,26 @@ skyrealm-descent;1157;;Skyrealm: Descent;;
|
||||
;1158;;远征:以太利亚;Expedition: Ætheria;
|
||||
;1159;;快速优化;Rapid Optimization;RO
|
||||
liminal-industries;1160;;阈限工业;Liminal Industries;
|
||||
;1161;;史诗冒险;;EA
|
||||
;1161;;史诗冒险;Epic Adventure;EA
|
||||
;1162;;根自日常;ROOTINE;
|
||||
blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;BACraft
|
||||
;1164;;Fresh & Smooth;;
|
||||
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
|
||||
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
|
||||
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
|
||||
;1168;;Polymania;;
|
||||
;1168;;Polymania!;;
|
||||
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
|
||||
beloong;1170;;化龍;BeLoong;
|
||||
;1171;;畅游自然;;SiF
|
||||
;1172;;退休工艺;Retirement Craft;RmC
|
||||
;1173;;随机枪械乱斗;NimShade's Random Gunfight;NRG
|
||||
;1173;;溯光暗潮:云影;NS's Random Gunfight;NRG
|
||||
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
|
||||
;1175;;硬核惊变;Hardcore Surprise;HS
|
||||
society-sunlit-valley;1176;;Society: Sunlit Valley;;
|
||||
ciscos-fantasy-medieval-rpg-dragonfyre;1177;;Cisco's Fantasy Medieval RPG [Dragonfyre];;
|
||||
all-the-mods-10-sky;1178;;All the Mods 10: To the Sky;;ATM10S
|
||||
;1179;;谐伴匠途;;
|
||||
;1181;;Turbo Vulkan;;TV
|
||||
;1181;;转变思路优化;New-Path Optimization;NO
|
||||
uls-zsurvival-craft;1182;;Z-Liminex;;
|
||||
;1183;;MITE 重生 3;MITE Reborn 3;MR
|
||||
reclamation-reclaim-the-world;1184;;Reclamation - Reclaim the World!;;
|
||||
@@ -1176,11 +1175,11 @@ realm-of-destiny;1192;;生命之境;Realm of Destiny;ROD
|
||||
pvzw;1195;;植物大战僵尸:世界;Plants VS Zombies : World;PVZW
|
||||
;1196;;农场物语Forge版;FarmingTales Forge;FT-WT
|
||||
new-century-adventure;1197;;新世纪之险;New Century Adventure;NCA
|
||||
repalmon;1198;;Re帕鲁梦;;RPLM
|
||||
repalmon;1198;;Re帕鲁梦;RePalmon;RPLM
|
||||
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
|
||||
arcaneaxis-ars;1200;;秘法象限;ArcaneAxis;ARS
|
||||
gregtech-easy;1201;;格雷科技简单版;GregTech Easy;GTE
|
||||
;1202;;Creation Exploring the Road of;;CETR
|
||||
;1202;;机境交响;Creation Exploring the Road of;CETR
|
||||
multiblockful-skies;1203;;Multiblockful Skies;;
|
||||
;1204;;刀剑物语;SwordStory;SS
|
||||
;1205;;无垠征途;The Unbounded Expedition;TUE
|
||||
@@ -1189,7 +1188,7 @@ custom-battleroyale-complete;1207;;完整自定义大逃杀;Custome BattleRoyale
|
||||
paradise-of-lucky-blocks;1208;;Paradise of Lucky Blocks;;
|
||||
;1209;;极致优化;Xavier Optimized;XO
|
||||
;1210;;神秘时代4花园;Thaumcraft IV Sky Islands;
|
||||
;1211;;弥散往生;Diffusion: Soulfare;
|
||||
;1211;;弥散往生;Diffusion: Soulfare;DSF
|
||||
mana-metal-new-horizons;1212;;魔法金屬:新視野;Mana Metal: New Horizons;M3NH
|
||||
farmy-skyblock;1213;;Farmy Skyblock;;
|
||||
bloody-skyblock-modpack;1214;;Bloody Skyblock;;
|
||||
@@ -1219,8 +1218,113 @@ linggango;1237;;Linggango;;
|
||||
create-industrialized-tech;1238;;Create: Industrialized Tech;;
|
||||
;1239;;灵璟铁路都市;LingJing Train City;LJTC
|
||||
erstwhile-journey-2;1240;;往昔之旅2;Erstwhile Journey 2;EJ2
|
||||
;1241;;诡厄:使徒;;
|
||||
;1241;;诡厄:使徒;;GRAE
|
||||
;1242;;时间之书 : 星之旅行-重制版;BookofTime StarTravel;BKSL
|
||||
cook-in-skyland-elegant-feast;1243;;天空厨房:典雅盛宴;Cook in Skyland Elegant Feast;CiS2
|
||||
ftb-stoneblock-4;1244;;FTB Stoneblock 4;;SB4
|
||||
ftb-stoneblock-4;1244;;FTB StoneBlock 4;;SB4
|
||||
techevdiscovery;1245;;TechEv || Discovery;;
|
||||
g3-project;1246;;G3 project;;
|
||||
;1247;;新州;Xin Zhou;XZ
|
||||
emc-and-fe-plus3;1248;;EMC & FE +++;;E&F2
|
||||
terracraft-confluence;1249;;泰拉工艺:汇流;TerraCraft:Confluence;TCC
|
||||
;1250;;探索领域;Explore Realm;ER
|
||||
beyond-cosmo;1251;;Beyond Cosmo;;
|
||||
;1252;;闲居物语;Travel Architecture Dreams;TAD
|
||||
high-altitude-alchemy;1253;;High-Altitude Alchemy;;
|
||||
;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
|
||||
skyopolis-evolved;1255;;Skyopolis: Evolved;;
|
||||
;1256;;IFR的1.12.2优化整合包;IFR's Forgified Optimization;IFO
|
||||
;1257;;无穷匠艺;;
|
||||
;1258;;Mersive.;;MS
|
||||
;1259;;GD656极致生电整合包;GD656 Ultimate Redstone;SD
|
||||
;1260;;登神者:天阶咏叹;The God Ascender;TGA
|
||||
;1261;;虚饰作品;Fictional;
|
||||
;1262;;无尽工坊:奇境传说;Infinity Workshop: Legends of Wonderland;IWLoW
|
||||
;1263;;大群方舟;Arknights: The Great Tide;AGT
|
||||
arcadia-rpg;1264;;Arcadia[RPG];;
|
||||
ozone-skyblock-reborn-2;1265;;Ozone Skyblock Reborn 2;;
|
||||
witarcland;1266;;维塔克兰;Witarcland;WTL
|
||||
big-globe-optimized-forge;1267;;Big Globe Optimized;;
|
||||
;1268;;GD656轻量优化打底整合包;GD656 Ultimate Optimized;GDO
|
||||
terra-firma-far-horizons;1269;;群峦传说:野望;Terra Firma Far Horizons;TFFH
|
||||
;1270;;烬蓝物语 2;BlueAsh Things II;BAT2
|
||||
anvilcraft-turbo;1271;;铁砧工艺:极速版;AnvilCraft-Turbo;ANCT
|
||||
jrs-tree-of-life;1272;;生命之树;Tree of life;
|
||||
reimagined-forge;1273;;Reimagined;;
|
||||
;1274;;MTR城市建设;MTR Urban Construction;MUC
|
||||
;1275;;Zecoar 整合包;Zecoar Modpack;ZM
|
||||
create-stranded-at-sea;1276;;Create: Stranded at Sea;;
|
||||
;1277;;胶兽异界之旅;;
|
||||
spellbound-skies;1278;;Spellbound Skies;;
|
||||
cozy-meadows;1279;;Cozy Meadows;;
|
||||
;1280;;无际旅人;EpochRealms;
|
||||
mythcrafts;1281;;MYTHCRAFT 5;;MC
|
||||
;1282;;牧场物语;Harvest Moon;
|
||||
;1283;;逃逸:寄生末日;SRP: Escape Parasite Doom;SRP:epd
|
||||
;1284;;Ice and M;;IAM
|
||||
;1285;;秋水;autumn floods;AF
|
||||
;1286;;V2O5;;
|
||||
;1287;;地牢冒险者;Dungeon Adventurer;DA
|
||||
ultramodern-optimization;1289;;超现代优化;Ultramodern optimization;UO
|
||||
reverie-foundry;1290;;遐想铸造厂;Reverie Foundry;RF
|
||||
;1291;;神秘时代4·全家桶;All the thaumcraft's mod;AllTC
|
||||
over-stars;1292;;Over Stars;;
|
||||
pandemonium-nexus-anarchic-aeon;1293;;万象枢纽:无序纪元;Pandemonium Nexus: Anarchic Aeon;PNAA
|
||||
;1294;;菌染亵渎;Fungal Infection Blasphemy;FIB
|
||||
;1295;;Ice and M:Sky;;IAMS
|
||||
;1296;;TUFF PERFORMANCE;;
|
||||
;1297;;灾厄巫咒;;
|
||||
;1298;;子弹呼啸 : 重制;Bullet Whizz : Remake;BWR
|
||||
mybiomesobeautiful;1299;;My Biomes Is So Beautiful;;
|
||||
create-capitalist;1300;;Create Capitalist;;
|
||||
diving-beyond;1301;;Diving Beyond;;
|
||||
;1302;;奇星·奥术;Mystic Star_Arcane;
|
||||
guild-adventures;1303;;Guild Adventures;;
|
||||
;1304;;艾斯爱慕:新视野;Spore and M:New Horizons;SMNH
|
||||
otherworld-dnd;1305;;Otherworld [Dungeons & Dragons];;
|
||||
;1306;;奇幻起源;Fantasy Origins;
|
||||
projects;1307;;ProjectS;;
|
||||
atheism;1308;;无神论;Atheism;
|
||||
;1309;;神之征伐;Apotheosis Conquest;
|
||||
the-adventure-of-valkyriens-2;1310;;瓦尔基里大冒险2;The Adventure of Valkyriens 2;TAOV2
|
||||
;1311;;超然多味;Various flavors of supernatural;vfos
|
||||
all-the-mons;1312;;All the Mons;;ATMons
|
||||
starskyblock;1313;;Star Sky Factory;;
|
||||
;1314;;拔刀之旅;Battou No Tabi;
|
||||
;1315;;物语;Stories;
|
||||
;1316;;圣末纪元;Holy End Era;HEE
|
||||
arcane-ascension;1317;;Arcane Ascension;;
|
||||
the-cordyceps-fungus;1318;;The Cordyceps Fungus;;
|
||||
bth-beyond-the-horizon;1319;;Beyond The Horizon;;BTH
|
||||
chosens-create-integrated;1320;;Chosen's Create: Integrated;;
|
||||
;1321;;织路;Travel Extensively;TE
|
||||
;1322;;MineZ v.无尽苦寒之地;MineZ v.I.C.E;
|
||||
;1323;;森语田园;;
|
||||
;1324;;子弹呼啸;Bullet Whizz;BW
|
||||
forgemechtrek;1325;;锻械游行;ForgeMechTrek;FMT
|
||||
;1326;;生电与优化整合包;Electricity Generation Integration Package;EGIP
|
||||
create-perfect-world-post-production;1327;;Create - Perfect World Post Production;;
|
||||
terrafirmagreg-modern;1328;;群峦格雷:现代版;TerraFirmaGreg: Modern;TFG
|
||||
nanomagic;1329;;NanoMagic;;
|
||||
;1330;;接入铁路网络:沉浸铁路;IR Railway Network Access (Immersive Railroading);RNA-IR
|
||||
biohazard-project-genesis;1331;;Biohazard: Project Genesis - The Ultimate Apocalypse;;
|
||||
;1332;;齐柏林的启示录;Apocalypse of Zeppelin;AoZ
|
||||
;1333;;遗尘往事;Story of Forgotten Land;SOFL
|
||||
farming-crossing-6-dinner-party;1334;;Farming Crossing 6: Dinner Party!;;
|
||||
;1335;;血族机械师;vampire mechanic;VM
|
||||
;1336;;维度行者:天启旅途;;
|
||||
;1337;;乌托邦探险之旅;Utopian Journey;UJ
|
||||
;1338;;统合防御·钢铁晨曦;United Defense: Steel Dawn;UDSD
|
||||
;1339;;机械动力:枪火不断;Create: Gunfire Never Stops;CGNS
|
||||
;1340;;逆转未来;Battle For The Future;BFF
|
||||
day-of-mind;1341;;DayOfMind;;DOM
|
||||
;1342;;机械动力:探险生存 2;Create Adventure Survival 2;
|
||||
;1343;;云游四海;WAYFARING;WF
|
||||
;1344;;阿卡迪亚的天启;The Apocalypse of Arcadia;TAOA
|
||||
;1345;;纯净原版・焕新;Pure Vanilla Renewal;PVR
|
||||
;1346;;命轮无章;Infinite Random;IR
|
||||
;1347;;星辉科技;StellarLightTech;STL
|
||||
dragns-o-suite-pack;1348;;DragN's O-Suite Pack;;
|
||||
;1349;;机械动力:工业与牧歌;Create: Industry and Pastoral;CIAP
|
||||
;1350;;灾厄纪行;;
|
||||
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BTS
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
@@ -18,14 +18,15 @@ dependencies {
|
||||
api(libs.gson)
|
||||
api(libs.toml)
|
||||
api(libs.xz)
|
||||
api(libs.lz4)
|
||||
api(libs.fx.gson)
|
||||
api(libs.constant.pool.scanner)
|
||||
api(libs.opennbt)
|
||||
api(libs.nanohttpd)
|
||||
api(libs.jsoup)
|
||||
api(libs.chardet)
|
||||
api(libs.jna)
|
||||
api(libs.pci.ids)
|
||||
api(libs.hello.nbt)
|
||||
|
||||
compileOnlyApi(libs.jetbrains.annotations)
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -57,6 +56,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
private final QuiltVersionList quilt;
|
||||
private final QuiltAPIVersionList quiltApi;
|
||||
private final List<Pair<String, String>> replacement;
|
||||
private final List<Pair<String, String>> fallbackReplacement;
|
||||
|
||||
public BMCLAPIDownloadProvider(String apiRoot) {
|
||||
this.apiRoot = apiRoot;
|
||||
@@ -73,7 +73,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
this.legacyFabric = new LegacyFabricVersionList(this);
|
||||
this.legacyFabricApi = new LegacyFabricAPIVersionList(this);
|
||||
|
||||
this.replacement = Arrays.asList(
|
||||
this.replacement = List.of(
|
||||
pair("https://bmclapi2.bangbang93.com", apiRoot),
|
||||
pair("https://launchermeta.mojang.com", apiRoot),
|
||||
pair("https://piston-meta.mojang.com", apiRoot),
|
||||
@@ -93,7 +93,10 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
pair("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
pair("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
||||
pair("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
|
||||
);
|
||||
|
||||
this.fallbackReplacement = List.of(
|
||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"),
|
||||
pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"),
|
||||
@@ -135,17 +138,38 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
private static String injectURL(List<Pair<String, String>> replacement, String baseURL) {
|
||||
for (Pair<String, String> pair : replacement) {
|
||||
if (baseURL.startsWith(pair.getKey())) {
|
||||
return pair.getValue() + baseURL.substring(pair.getKey().length());
|
||||
}
|
||||
}
|
||||
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
return injectURL(replacement, baseURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URI> injectURLWithCandidates(String baseURL) {
|
||||
String injected = injectURL(replacement, baseURL);
|
||||
if (injected.equals(baseURL)) {
|
||||
String fallbackInjected = injectURL(fallbackReplacement, baseURL);
|
||||
if (fallbackInjected.equals(baseURL)) {
|
||||
return List.of(NetworkUtils.toURI(baseURL));
|
||||
} else {
|
||||
return List.of(
|
||||
NetworkUtils.toURI(baseURL),
|
||||
NetworkUtils.toURI(fallbackInjected)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return List.of(NetworkUtils.toURI(injected));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConcurrency() {
|
||||
return Math.max(Runtime.getRuntime().availableProcessors() * 2, 6);
|
||||
|
||||
@@ -42,7 +42,7 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
|
||||
@Override
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "P7dR8mSH"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
|
||||
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user