Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57a41c2f3 | ||
|
|
d63b0bb088 | ||
|
|
8e9ddfb248 | ||
|
|
1edd36819c | ||
|
|
a5f6028b3a | ||
|
|
52f57c783e | ||
|
|
77a86ef963 | ||
|
|
32a741e2de | ||
|
|
d1ead74177 | ||
|
|
80ba1ec1a7 | ||
|
|
25fef6b5c2 | ||
|
|
64ce1854f5 | ||
|
|
fb4f0be899 | ||
|
|
eea072cea5 | ||
|
|
b0cd184e22 | ||
|
|
8b3e801a44 | ||
|
|
04bb089f3a | ||
|
|
4bd0cbc88f | ||
|
|
9c4c7bd786 | ||
|
|
f79d560605 | ||
|
|
90e6782193 | ||
|
|
ca071fd924 | ||
|
|
2e26b14322 | ||
|
|
b7fbebae43 | ||
|
|
33dc39e59e | ||
|
|
a70b8d7baa | ||
|
|
64821600c5 | ||
|
|
dc993b871f | ||
|
|
a0bd64ad1f | ||
|
|
10df7a862b | ||
|
|
9ea6a97aef | ||
|
|
dec2e4ec7a | ||
|
|
8d504cd194 | ||
|
|
2918f19482 | ||
|
|
18f3e2870b | ||
|
|
9f2874c12b | ||
|
|
3fdf21fcd5 | ||
|
|
c9e435c129 | ||
|
|
26125f4002 | ||
|
|
93be558e84 | ||
|
|
e1be52f0f0 | ||
|
|
359a90636a | ||
|
|
d9b31ffd18 | ||
|
|
4b72639969 | ||
|
|
aecbf2e476 | ||
|
|
52d8c44320 | ||
|
|
5d57052ea2 | ||
|
|
570e1e2029 | ||
|
|
5b11202a9c | ||
|
|
c2d5d1b5f6 | ||
|
|
ef5849eba1 | ||
|
|
c2c542af3c | ||
|
|
792412b4c7 | ||
|
|
2b8e8306d7 | ||
|
|
84bcb01fe1 | ||
|
|
253bdf974e | ||
|
|
5839382c64 | ||
|
|
ab9c45549b | ||
|
|
cb413fa40a | ||
|
|
982c062a01 | ||
|
|
9f097149b2 | ||
|
|
cb50c89fb9 | ||
|
|
7fb2b9b734 | ||
|
|
b933fbcd6b | ||
|
|
c880e4befb | ||
|
|
575d908dba | ||
|
|
ecf188ad18 | ||
|
|
0b3cb9964a | ||
|
|
c175f1c1b3 | ||
|
|
dbacf096c6 | ||
|
|
c7b9b73292 | ||
|
|
19079ac123 | ||
|
|
2a66880a58 | ||
|
|
8b91251337 | ||
|
|
0c2742a895 | ||
|
|
a8692c3893 |
No files matched your search
@@ -28,7 +28,6 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||
@@ -60,6 +59,7 @@ dependencies {
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
implementation(libs.nayuki.qrcodegen)
|
||||
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
@@ -152,7 +152,6 @@ val hmclProperties = buildList {
|
||||
}
|
||||
add("hmcl.version.type" to versionType)
|
||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,10 @@ public class JFXPopup extends PopupControl {
|
||||
/// @param initOffsetX on the x-axis
|
||||
/// @param initOffsetY on the y-axis
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
|
||||
}
|
||||
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
|
||||
if (!isShowing()) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null || scene.getWindow() == null) {
|
||||
@@ -137,10 +141,13 @@ public class JFXPopup extends PopupControl {
|
||||
|
||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
||||
|
||||
this.show(node,
|
||||
parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0),
|
||||
parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0)
|
||||
);
|
||||
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
||||
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
||||
|
||||
if (attachToNode)
|
||||
this.show(node, anchorX, anchorY);
|
||||
else
|
||||
this.show(parent, anchorX, anchorY);
|
||||
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
/// <ol>
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
///
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
/// </ol>
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
|
||||
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
|
||||
///
|
||||
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
|
||||
public class JFXSnackbar extends Group {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
|
||||
|
||||
private Pane snackbarContainer;
|
||||
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
|
||||
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
|
||||
|
||||
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
|
||||
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Pane content;
|
||||
private PseudoClass activePseudoClass = null;
|
||||
private PauseTransition pauseTransition;
|
||||
|
||||
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
|
||||
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
|
||||
///
|
||||
///
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
public JFXSnackbar() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
|
||||
/// the [#enqueue(SnackbarEvent)] method works correctly.
|
||||
public JFXSnackbar(Pane snackbarContainer) {
|
||||
initialize();
|
||||
content = new StackPane();
|
||||
content.getStyleClass().add("jfx-snackbar-content");
|
||||
//wrap the content in a group so that the content is managed inside its own container
|
||||
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
|
||||
getChildren().add(content);
|
||||
setManaged(false);
|
||||
setVisible(false);
|
||||
|
||||
// register the container before resizing it
|
||||
registerSnackbarContainer(snackbarContainer);
|
||||
|
||||
// resize the popup if its layout has been changed
|
||||
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
|
||||
|
||||
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
// Setters / Getters
|
||||
|
||||
public Pane getPopupContainer() {
|
||||
return snackbarContainer;
|
||||
}
|
||||
|
||||
public void setPrefWidth(double width) {
|
||||
content.setPrefWidth(width);
|
||||
}
|
||||
|
||||
public double getPrefWidth() {
|
||||
return content.getPrefWidth();
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
public void registerSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer != null) {
|
||||
//since listeners are added the container should be properly registered/unregistered
|
||||
throw new IllegalArgumentException("Snackbar Container already set");
|
||||
}
|
||||
this.snackbarContainer = snackbarContainer;
|
||||
this.snackbarContainer.getChildren().add(this);
|
||||
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer == null) {
|
||||
throw new IllegalArgumentException("Snackbar Container not set");
|
||||
}
|
||||
this.snackbarContainer.getChildren().remove(this);
|
||||
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void show(SnackbarEvent event) {
|
||||
content.getChildren().setAll(event.getContent());
|
||||
openAnimation = getTimeline(event.getTimeout());
|
||||
if (event.getPseudoClass() != null) {
|
||||
activePseudoClass = event.getPseudoClass();
|
||||
content.pseudoClassStateChanged(activePseudoClass, true);
|
||||
}
|
||||
openAnimation.play();
|
||||
}
|
||||
|
||||
private Timeline openAnimation = null;
|
||||
|
||||
private Timeline getTimeline(Duration timeout) {
|
||||
Timeline animation;
|
||||
animation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(10),
|
||||
e -> {
|
||||
this.toFront();
|
||||
this.setVisible(true);
|
||||
}
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
animation.setCycleCount(1);
|
||||
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
|
||||
if (pauseTransition != null) {
|
||||
animation.setOnFinished(finish -> {
|
||||
pauseTransition.setOnFinished(done -> {
|
||||
pauseTransition = null;
|
||||
eventsSet.remove(currentEvent);
|
||||
currentEvent = eventQueue.peek();
|
||||
close();
|
||||
});
|
||||
pauseTransition.play();
|
||||
});
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (openAnimation != null) {
|
||||
openAnimation.stop();
|
||||
}
|
||||
if (this.isVisible()) {
|
||||
Timeline closeAnimation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> this.toFront(),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(290),
|
||||
e -> this.setVisible(true)
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
closeAnimation.setCycleCount(1);
|
||||
closeAnimation.setOnFinished(e -> {
|
||||
resetPseudoClass();
|
||||
processSnackbar();
|
||||
});
|
||||
closeAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
private SnackbarEvent currentEvent = null;
|
||||
|
||||
public SnackbarEvent getCurrentEvent() {
|
||||
return currentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
|
||||
*
|
||||
* @param event the {@link SnackbarEvent event} to put in the queue.
|
||||
*/
|
||||
public void enqueue(SnackbarEvent event) {
|
||||
synchronized (this) {
|
||||
if (!eventsSet.contains(event)) {
|
||||
eventsSet.add(event);
|
||||
eventQueue.offer(event);
|
||||
} else if (currentEvent == event && pauseTransition != null) {
|
||||
pauseTransition.playFromStart();
|
||||
}
|
||||
}
|
||||
if (processingQueue.compareAndSet(false, true)) {
|
||||
Platform.runLater(() -> {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
show(currentEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void resetPseudoClass() {
|
||||
if (activePseudoClass != null) {
|
||||
content.pseudoClassStateChanged(activePseudoClass, false);
|
||||
activePseudoClass = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void processSnackbar() {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
eventsSet.remove(currentEvent);
|
||||
show(currentEvent);
|
||||
} else {
|
||||
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
|
||||
//should not be a race condition
|
||||
processingQueue.getAndSet(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPopup() {
|
||||
if (snackbarContainer == null) {
|
||||
return;
|
||||
}
|
||||
Bounds contentBound = this.getLayoutBounds();
|
||||
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
|
||||
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
|
||||
this.setLayoutX(offsetX);
|
||||
this.setLayoutY(offsetY);
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Event API
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
|
||||
///
|
||||
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
|
||||
///
|
||||
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
|
||||
/// confused with the [java.time.Duration].
|
||||
public static class SnackbarEvent extends Event {
|
||||
|
||||
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
|
||||
|
||||
/// The amount of time the snackbar will show for, if not otherwise specified.
|
||||
///
|
||||
/// It's 1.5 seconds.
|
||||
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
|
||||
|
||||
private final Node content;
|
||||
private final PseudoClass pseudoClass;
|
||||
private final Duration timeout;
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content) {
|
||||
this(content, DEFAULT_DURATION, null);
|
||||
}
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
|
||||
this(content, DEFAULT_DURATION, pseudoClass);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout) {
|
||||
this(content, timeout, null);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
|
||||
///
|
||||
/// If you don't need so much customization, try one of the other constructors.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
|
||||
super(SNACKBAR);
|
||||
this.content = content;
|
||||
this.pseudoClass = pseudoClass;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public PseudoClass getPseudoClass() {
|
||||
return pseudoClass;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public EventType<? extends SnackbarEvent> getEventType() {
|
||||
return (EventType<? extends SnackbarEvent>) super.getEventType();
|
||||
}
|
||||
|
||||
public boolean isPersistent() {
|
||||
return Duration.INDEFINITE.equals(getTimeout());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
/// JFXSnackbarLayout default layout for snackbar content
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2018-11-16
|
||||
public class JFXSnackbarLayout extends BorderPane {
|
||||
|
||||
private final Label toast;
|
||||
private JFXButton action;
|
||||
private final StackPane actionContainer;
|
||||
|
||||
public JFXSnackbarLayout(String message) {
|
||||
this(message, null, null);
|
||||
}
|
||||
|
||||
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
|
||||
initialize();
|
||||
|
||||
toast = new Label();
|
||||
toast.setMinWidth(Control.USE_PREF_SIZE);
|
||||
toast.getStyleClass().add("jfx-snackbar-toast");
|
||||
toast.setWrapText(true);
|
||||
toast.setText(message);
|
||||
StackPane toastContainer = new StackPane(toast);
|
||||
toastContainer.setPadding(new Insets(20));
|
||||
actionContainer = new StackPane();
|
||||
actionContainer.setPadding(new Insets(0, 10, 0, 0));
|
||||
|
||||
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
if (getPrefWidth() == -1) {
|
||||
return getPrefWidth();
|
||||
}
|
||||
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
|
||||
return prefWidthProperty().get() - actionWidth;
|
||||
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
|
||||
|
||||
setLeft(toastContainer);
|
||||
setRight(actionContainer);
|
||||
|
||||
if (actionText != null) {
|
||||
action = new JFXButton();
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
action.setMinWidth(Control.USE_PREF_SIZE);
|
||||
action.setButtonType(JFXButton.ButtonType.FLAT);
|
||||
action.getStyleClass().add("jfx-snackbar-action");
|
||||
// actions will be added upon showing the snackbar if needed
|
||||
actionContainer.getChildren().add(action);
|
||||
|
||||
if (!actionText.isEmpty()) {
|
||||
action.setVisible(true);
|
||||
actionContainer.setVisible(true);
|
||||
actionContainer.setManaged(true);
|
||||
// to force updating the layout bounds
|
||||
action.setText("");
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
} else {
|
||||
actionContainer.setVisible(false);
|
||||
actionContainer.setManaged(false);
|
||||
action.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
|
||||
|
||||
public String getToast() {
|
||||
return toast.getText();
|
||||
}
|
||||
|
||||
public void setToast(String toast) {
|
||||
this.toast.setText(toast);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,17 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
@@ -95,14 +98,17 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
|
||||
rgbField.getStyleClass().add("custom-color-field");
|
||||
rgbField.setPromptText("RGB Color");
|
||||
rgbField.setTextFormatter(colorCharFormatter());
|
||||
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hsbField.getStyleClass().add("custom-color-field");
|
||||
hsbField.setPromptText("HSB Color");
|
||||
hsbField.setTextFormatter(colorCharFormatter());
|
||||
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hexField.getStyleClass().add("custom-color-field");
|
||||
hexField.setPromptText("#HEX Color");
|
||||
hexField.setTextFormatter(colorCharFormatter());
|
||||
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
StackPane tabContent = new StackPane();
|
||||
@@ -404,4 +410,20 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
dialog.setMinWidth(minWidth);
|
||||
dialog.setMinHeight(minHeight);
|
||||
}
|
||||
|
||||
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
|
||||
|
||||
private static TextFormatter<String> colorCharFormatter() {
|
||||
return new TextFormatter<>(change -> {
|
||||
if (!change.isContentChange()) return change;
|
||||
|
||||
String ins = StringUtils.toHalfWidth(change.getText());
|
||||
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
|
||||
String full = StringUtils.toHalfWidth(change.getControlNewText());
|
||||
long h = full.chars().filter(c -> c == '#').count();
|
||||
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
|
||||
change.setText(ins);
|
||||
return change;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,28 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
private final String codeVerifier;
|
||||
private final String state;
|
||||
|
||||
public static String lastlyOpenedURL;
|
||||
|
||||
@@ -52,6 +53,34 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
|
||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
var random = new SecureRandom();
|
||||
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
this.state = encoder.encodeToString(bytes);
|
||||
}
|
||||
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
||||
byte[] bytes = new byte[64];
|
||||
random.nextBytes(bytes);
|
||||
this.codeVerifier = encoder.encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCodeVerifier() {
|
||||
return codeVerifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,19 +122,32 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
String parameters = session.getQueryParameterString();
|
||||
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||
if (query.containsKey("code")) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(query.get("code"));
|
||||
|
||||
String code = query.get("code");
|
||||
if (code != null) {
|
||||
if (this.state.equals(query.get("state"))) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(code);
|
||||
} else if (query.containsKey("state")) {
|
||||
LOG.warning("Failed to authenticate: invalid state in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
|
||||
} else {
|
||||
LOG.warning("Failed to authenticate: missing state in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
|
||||
}
|
||||
} else {
|
||||
LOG.warning("Error: " + parameters);
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
LOG.warning("Failed to authenticate: missing authorization code in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
.replace("%success%", i18n("message.success"))
|
||||
.replace("%ok%", i18n("button.ok"))
|
||||
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load html", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
@@ -123,7 +165,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
@@ -150,17 +193,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
|
||||
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -168,17 +207,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublicClient() {
|
||||
return true; // We have turned on the device auth flow.
|
||||
}
|
||||
}
|
||||
|
||||
public static class GrantDeviceCodeEvent extends Event {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
@@ -44,13 +45,15 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -310,11 +313,18 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of(
|
||||
"player", getPlayerName()
|
||||
));
|
||||
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
|
||||
)).setSignificance(Task.TaskSignificance.MINOR);
|
||||
}).start();
|
||||
|
||||
new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
@@ -323,15 +333,19 @@ public final class TerracottaManager {
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of(
|
||||
"room", room,
|
||||
"player", getPlayerName()
|
||||
));
|
||||
|
||||
return new GetTask(uri)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class TerracottaNodeList {
|
||||
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
|
||||
|
||||
@JsonSerializable
|
||||
private record TerracottaNode(String url, @Nullable String region) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile List<URI> list;
|
||||
|
||||
public static List<URI> fetch() {
|
||||
List<URI> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
synchronized (TerracottaNodeList.class) {
|
||||
list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
try {
|
||||
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
|
||||
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
|
||||
|
||||
if (nodes == null) {
|
||||
list = List.of();
|
||||
LOG.info("No available Terracotta nodes found");
|
||||
} else {
|
||||
list = nodes.stream()
|
||||
.filter(node -> {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
node.validate();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid terracotta node: " + node, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> URI.create(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to fetch terracotta node list", e);
|
||||
list = List.of();
|
||||
}
|
||||
|
||||
TerracottaNodeList.list = list;
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private TerracottaNodeList() {
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -49,10 +49,10 @@ public final class DialogController {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
||||
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
}, latch::countDown, false);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
latch.await();
|
||||
|
||||
@@ -312,14 +312,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
return limitingSize(node, 30, 20);
|
||||
}
|
||||
|
||||
public static Node wrap(SVG svg) {
|
||||
return wrap(svg.createIcon(20));
|
||||
}
|
||||
|
||||
private static class ListenerPair<T> {
|
||||
private final ObservableValue<T> value;
|
||||
private final ChangeListener<? super T> listener;
|
||||
|
||||
@@ -194,8 +194,9 @@ public class InstallerItem extends Control {
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom, liteLoader, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabricApi, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, legacyfabricApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, quilt, quiltApi);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
@@ -342,6 +343,7 @@ public class InstallerItem extends Control {
|
||||
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
|
||||
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setPickOnBounds(false);
|
||||
buttonsContainer.setSpacing(8);
|
||||
buttonsContainer.setAlignment(Pos.CENTER);
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
@@ -18,22 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
/**
|
||||
* All vector icons used in the launcher.
|
||||
* <p>
|
||||
* Unless otherwise stated,
|
||||
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
|
||||
*/
|
||||
/// All vector icons used in the launcher.
|
||||
///
|
||||
/// Unless otherwise stated,
|
||||
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
/// The view boxes of all icons are normalized to `0 0 24 24`.
|
||||
public enum SVG {
|
||||
NONE(""), // Empty Icon
|
||||
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
|
||||
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
|
||||
@@ -50,9 +45,9 @@ public enum SVG {
|
||||
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
|
||||
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
|
||||
CONTENT_CUT("m12 14l-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22t-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12L7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6t1.175-2.825T6 2t2.825 1.175T10 6q0 .425-.075.85t-.275.8L20.6 18.6q.675.675.3 1.538T19.575 21q-.275 0-.537-.112t-.463-.313zm3-3l-2-2l5.575-5.575q.2-.2.463-.312T19.574 3q.95 0 1.313.875t-.313 1.55zM6 8q.825 0 1.413-.587T8 6t-.587-1.412T6 4t-1.412.588T4 6t.588 1.413T6 8m6 4.5q.2 0 .35-.15t.15-.35t-.15-.35t-.35-.15t-.35.15t-.15.35t.15.35t.35.15M6 20q.825 0 1.413-.587T8 18t-.587-1.412T6 16t-1.412.588T4 18t.588 1.413T6 20"),
|
||||
CONTENT_CUT("M19 21l-7-7-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22q-1.65 0-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12 7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6q0-1.65 1.175-2.825T6 2q1.65 0 2.825 1.175T10 6q0 .425-.075.85t-.275.8L22 20v1H19Zm-4-10-2-2 6-6h3v1l-7 7ZM7.4125 7.4125Q8 6.825 8 6t-.5875-1.4125Q6.825 4 6 4t-1.4125.5875Q4 5.175 4 6t.5875 1.4125T6 8t1.4125-.5875ZM12.35 12.35q.15-.15.15-.35t-.15-.35-.35-.15-.35.15-.15.35.15.35.35.15.35-.15ZM7.4125 19.4125Q8 18.825 8 18t-.5875-1.4125Q6.825 16 6 16t-1.4125.5875Q4 17.175 4 18t.5875 1.4125Q5.175 20 6 20t1.4125-.5875Z"),
|
||||
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v2q0 .425-.288.713T16 8H8q-.425 0-.712-.288T7 7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
@@ -92,8 +87,7 @@ public enum SVG {
|
||||
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
|
||||
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
|
||||
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
|
||||
MINIMIZE("M6 21V19H18V21H6Z"),
|
||||
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
|
||||
MINIMIZE_CENTER("M6 13v-2h12v2H6Z"), // Not Material
|
||||
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
|
||||
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
|
||||
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
|
||||
@@ -104,7 +98,7 @@ public enum SVG {
|
||||
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
|
||||
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
|
||||
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
|
||||
REDO("M16.2 10H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H16q.425 0 .713.288T17 18t-.288.713T16 19H9.9q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-1.9-1.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.6 3.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.6 3.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"),
|
||||
REDO("M9.9 19q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-2.6-2.6L15 4l5 5l-5 5l-1.4-1.4l2.6-2.6H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H17v2z"),
|
||||
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
@@ -120,12 +114,12 @@ public enum SVG {
|
||||
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
|
||||
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
|
||||
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
|
||||
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
|
||||
TEXTURE("M4.4 21q-.475-.1-.8875-.5125T3 19.6L19.6 3q.525.125.9.5125t.525.8875L4.4 21ZM3 14.7v-2.8L11.9 3h2.8L3 14.7ZM3 7V5q0-.825.5875-1.4125T5 3h2L3 7Zm14 14 4-4v2q0 .825-.5875 1.4125T19 21h-2Zm-7.7 0L21 9.3v2.8L12.1 21H9.3Z"),
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
UNFOLD_MORE("M12 21 7.5 16.5l1.45-1.45L12 18.1l3.05-3.05 1.45 1.45L12 21ZM8.95 9.05 7.5 7.6 12 3.1l4.5 4.5-1.45 1.45L12 6 8.95 9.05Z"),
|
||||
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
|
||||
UNDO("M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
|
||||
UNDO("M7 19v-2h7.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l2.6 2.6L9 14L4 9l5-5l1.4 1.4L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
|
||||
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
@@ -135,48 +129,40 @@ public enum SVG {
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
|
||||
private final String path;
|
||||
static void setSize(SVGPath path, double size) {
|
||||
double scale = size / DEFAULT_SIZE;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
}
|
||||
|
||||
SVG(String path) {
|
||||
this.path = path;
|
||||
private final String rawPath;
|
||||
private String path;
|
||||
|
||||
SVG(String rawPath) {
|
||||
this.rawPath = rawPath;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
if (path == null)
|
||||
// We move the current point so that SVGPath will treat 0 0 24 24 as the layout bounds
|
||||
path = "M24 24ZM0 0Z" + rawPath;
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Node createIcon(SVGPath path, double size) {
|
||||
if (size < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
return pane;
|
||||
}
|
||||
|
||||
double scale = size / 24;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
return new Group(path);
|
||||
public SVGPath createIcon() {
|
||||
var path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(getPath());
|
||||
return path;
|
||||
}
|
||||
|
||||
public Node createIcon(double size) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, size);
|
||||
public SVGContainer createIcon(double size) {
|
||||
return new SVGContainer(this, size);
|
||||
}
|
||||
|
||||
public Node createIcon() {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, -1);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
p.fillProperty().bind(color);
|
||||
return createIcon(p, -1);
|
||||
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath path = createIcon();
|
||||
path.fillProperty().bind(color);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// A lightweight wrapper for displaying [SVG] icons.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class SVGContainer extends Parent {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "svg-container";
|
||||
|
||||
private final SVGPath path = new SVGPath();
|
||||
private SVG icon = SVG.NONE;
|
||||
private double iconSize = SVG.DEFAULT_SIZE;
|
||||
private SVGPath tempPath;
|
||||
private Timeline timeline;
|
||||
|
||||
{
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.path.getStyleClass().add("svg");
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the default icon and the default icon size.
|
||||
public SVGContainer() {
|
||||
this(SVG.NONE, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer showing the given icon using the default icon size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
public SVGContainer(SVG icon) {
|
||||
this(icon, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with a custom icon size. The initial icon is
|
||||
/// [SVG#NONE].
|
||||
///
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(double iconSize) {
|
||||
this(SVG.NONE, iconSize);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the specified icon and size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(SVG icon, double iconSize) {
|
||||
setIconSizeImpl(iconSize);
|
||||
setIcon(icon);
|
||||
}
|
||||
|
||||
public double getIconSize() {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
private void setIconSizeImpl(double newSize) {
|
||||
this.iconSize = newSize;
|
||||
SVG.setSize(path, newSize);
|
||||
if (tempPath != null)
|
||||
SVG.setSize(tempPath, newSize);
|
||||
}
|
||||
|
||||
public void setIconSize(double newSize) {
|
||||
setIconSizeImpl(newSize);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/// Gets the currently displayed icon.
|
||||
public SVG getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/// Sets the icon to display without animation.
|
||||
public void setIcon(SVG newIcon) {
|
||||
setIcon(newIcon, Duration.ZERO);
|
||||
}
|
||||
|
||||
/// Sets the icon to display with a cross-fade animation.
|
||||
public void setIcon(SVG newIcon, Duration animationDuration) {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline = null;
|
||||
}
|
||||
|
||||
SVG oldIcon = this.icon;
|
||||
this.icon = newIcon;
|
||||
|
||||
if (animationDuration.equals(Duration.ZERO)) {
|
||||
path.setContent(newIcon.getPath());
|
||||
path.setOpacity(1);
|
||||
if (getChildren().size() != 1)
|
||||
getChildren().setAll(path);
|
||||
} else {
|
||||
if (tempPath == null) {
|
||||
tempPath = new SVGPath();
|
||||
tempPath.getStyleClass().add("svg");
|
||||
SVG.setSize(tempPath, iconSize);
|
||||
} else
|
||||
tempPath.setOpacity(1);
|
||||
|
||||
tempPath.setContent(oldIcon.getPath());
|
||||
getChildren().setAll(path, tempPath);
|
||||
|
||||
path.setOpacity(0);
|
||||
path.setContent(newIcon.getPath());
|
||||
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
|
||||
),
|
||||
new KeyFrame(animationDuration,
|
||||
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
|
||||
)
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
getChildren().setAll(path);
|
||||
timeline = null;
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Parent
|
||||
|
||||
@Override
|
||||
public double prefWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double prefHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,14 +42,11 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
public ToolbarListPageSkin(P skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
|
||||
StackPane container = new StackPane();
|
||||
container.getChildren().add(root);
|
||||
StackPane.setMargin(root, new Insets(10));
|
||||
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
@@ -61,32 +58,33 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
|
||||
ComponentList.setVgrow(spinnerPane, Priority.ALWAYS);
|
||||
|
||||
{
|
||||
this.listView = new JFXListView<>();
|
||||
this.listView.setPadding(Insets.EMPTY);
|
||||
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
|
||||
ComponentList.setVgrow(listView, Priority.ALWAYS);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
root.getContent().add(listView);
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
}
|
||||
|
||||
spinnerPane.setContent(root);
|
||||
root.getContent().add(spinnerPane);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.setPadding(new Insets(0, 5, 0, 2));
|
||||
stackPane.getChildren().setAll(node);
|
||||
return stackPane;
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setGraphic(svg.createIcon(20));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
@@ -95,7 +93,7 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setGraphic(svg.createIcon(20));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
|
||||
@@ -20,14 +20,19 @@ package org.jackhuang.hmcl.ui;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WeakListenerHolder {
|
||||
public final class WeakListenerHolder {
|
||||
private final List<Object> refs = new ArrayList<>(0);
|
||||
|
||||
public WeakListenerHolder() {
|
||||
@@ -48,6 +53,22 @@ public class WeakListenerHolder {
|
||||
return new WeakListChangeListener<>(listener);
|
||||
}
|
||||
|
||||
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer) {
|
||||
refs.add(manager.registerWeak(consumer));
|
||||
}
|
||||
|
||||
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer, EventPriority priority) {
|
||||
refs.add(manager.registerWeak(consumer, priority));
|
||||
}
|
||||
|
||||
public <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
refs.add(FXUtils.onWeakChange(value, consumer));
|
||||
}
|
||||
|
||||
public <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
refs.add(FXUtils.onWeakChangeAndOperate(value, consumer));
|
||||
}
|
||||
|
||||
public void add(Object obj) {
|
||||
refs.add(obj);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -76,9 +77,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
canvas = new Canvas(32, 32);
|
||||
setLeftGraphic(canvas);
|
||||
canvas.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(canvas, Pos.CENTER);
|
||||
|
||||
setActionButtonVisible(false);
|
||||
setLeftGraphic(canvas);
|
||||
|
||||
if (account != null) {
|
||||
this.accountProperty().set(account);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
@@ -54,7 +53,6 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -130,35 +128,25 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
microsoftItem.setLeftIcon(SVG.MICROSOFT);
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
|
||||
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
|
||||
offlineItem.setLeftIcon(SVG.PERSON);
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setLeftGraphic(wrap(SVG.DRESSER));
|
||||
item.setLeftIcon(SVG.DRESSER);
|
||||
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnAction(e -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
}, null);
|
||||
e.consume();
|
||||
});
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
item.setRightAction(SVG.CLOSE, () -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
}, null));
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
@@ -206,8 +194,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
|
||||
addAuthServerItem.setTitle(i18n("account.injector.add"));
|
||||
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
|
||||
addAuthServerItem.setActionButtonVisible(false);
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG.ADD_CIRCLE));
|
||||
addAuthServerItem.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
|
||||
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private final JFXButton btnAccept;
|
||||
private final SpinnerPane spinner;
|
||||
private final Node body;
|
||||
|
||||
private final HBox actions;
|
||||
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
|
||||
private final Pane detailsContainer;
|
||||
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
btnCancel.setOnAction(e -> onCancel());
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
|
||||
HBox hbox = new HBox(spinner, btnCancel);
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
actions = new HBox(spinner, btnCancel);
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setActions(lblErrorMessage, hbox);
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
if (showMethodSwitcher) {
|
||||
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
|
||||
Runnable doCreate = () -> {
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
@@ -280,93 +273,16 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
btnAccept.disableProperty().unbind();
|
||||
detailsContainer.getChildren().remove(detailsPane);
|
||||
lblErrorMessage.setText("");
|
||||
lblErrorMessage.setVisible(true);
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
detailsPane = vbox;
|
||||
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
vbox.getChildren().add(hintPane);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("unofficial.hint"));
|
||||
vbox.getChildren().add(hintPane);
|
||||
}
|
||||
|
||||
VBox codeBox = new VBox(8);
|
||||
Label hint = new Label(i18n("account.methods.microsoft.code"));
|
||||
Label code = new Label();
|
||||
code.setMouseTransparent(true);
|
||||
code.setStyle("-fx-font-size: 24");
|
||||
codeBox.getChildren().addAll(hint, code);
|
||||
codeBox.setAlignment(Pos.CENTER);
|
||||
vbox.getChildren().add(codeBox);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||
errHintPane.setVisible(false);
|
||||
errHintPane.setManaged(false);
|
||||
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
code.setText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
|
||||
codeBox.setVisible(true);
|
||||
codeBox.setManaged(true);
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
}
|
||||
});
|
||||
|
||||
lblErrorMessage.setVisible(false);
|
||||
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
|
||||
boolean hasError = !newVal.isEmpty();
|
||||
errHintPane.setSegment(newVal);
|
||||
errHintPane.setVisible(hasError);
|
||||
errHintPane.setManaged(hasError);
|
||||
hintPane.setVisible(!hasError);
|
||||
hintPane.setManaged(!hasError);
|
||||
codeBox.setVisible(!hasError && deviceCode.get() != null);
|
||||
codeBox.setManaged(!hasError && deviceCode.get() != null);
|
||||
});
|
||||
|
||||
FXUtils.onClicked(codeBox, () -> {
|
||||
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
|
||||
runInFX(() -> deviceCode.set(value))
|
||||
));
|
||||
|
||||
HBox linkBox = new HBox();
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
||||
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
|
||||
|
||||
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
|
||||
btnAccept.setDisable(false);
|
||||
detailsPane = new MicrosoftAccountLoginPane(true);
|
||||
setActions();
|
||||
} else {
|
||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
detailsContainer.getChildren().add(detailsPane);
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import io.nayuki.qrcodegen.QrCode;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.QrCodeUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware {
|
||||
private final Account accountToRelogin;
|
||||
private final Consumer<AuthInfo> loginCallback;
|
||||
private final Runnable cancelCallback;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private final ObjectProperty<Step> step = new SimpleObjectProperty<>();
|
||||
|
||||
private TaskExecutor browserTaskExecutor;
|
||||
private TaskExecutor deviceTaskExecutor;
|
||||
|
||||
private final JFXButton btnLogin;
|
||||
private final SpinnerPane loginButtonSpinner;
|
||||
|
||||
public MicrosoftAccountLoginPane() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public MicrosoftAccountLoginPane(boolean bodyonly) {
|
||||
this(null, null, null, bodyonly);
|
||||
}
|
||||
|
||||
public MicrosoftAccountLoginPane(Account account, Consumer<AuthInfo> callback, Runnable onCancel, boolean bodyonly) {
|
||||
this.accountToRelogin = account;
|
||||
this.loginCallback = callback;
|
||||
this.cancelCallback = onCancel;
|
||||
|
||||
getStyleClass().add("microsoft-login-dialog");
|
||||
if (bodyonly) {
|
||||
this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true);
|
||||
} else {
|
||||
Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft"));
|
||||
heading.getStyleClass().add("header-label");
|
||||
setHeading(heading);
|
||||
}
|
||||
|
||||
this.setMaxWidth(650);
|
||||
|
||||
onEscPressed(this, this::onCancel);
|
||||
|
||||
btnLogin = new JFXButton(i18n("account.login"));
|
||||
btnLogin.getStyleClass().add("dialog-accept");
|
||||
|
||||
loginButtonSpinner = new SpinnerPane();
|
||||
loginButtonSpinner.getStyleClass().add("small-spinner-pane");
|
||||
loginButtonSpinner.setContent(btnLogin);
|
||||
|
||||
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
btnCancel.setOnAction(e -> onCancel());
|
||||
|
||||
setActions(loginButtonSpinner, btnCancel);
|
||||
|
||||
holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> {
|
||||
if (step.get() instanceof Step.StartAuthorizationCodeLogin)
|
||||
step.set(new Step.WaitForOpenBrowser(event.getUrl()));
|
||||
}));
|
||||
|
||||
holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> {
|
||||
if (step.get() instanceof Step.StartDeviceCodeLogin)
|
||||
step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri()));
|
||||
}));
|
||||
|
||||
this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty()
|
||||
? new Step.Init()
|
||||
: new Step.StartAuthorizationCodeLogin());
|
||||
FXUtils.onChangeAndOperate(step, this::onStep);
|
||||
}
|
||||
|
||||
private void onStep(Step currentStep) {
|
||||
VBox rootContainer = new VBox(10);
|
||||
setBody(rootContainer);
|
||||
rootContainer.setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
rootContainer.getChildren().add(snapshotHint);
|
||||
btnLogin.setDisable(true);
|
||||
loginButtonSpinner.setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHintPane.setSegment(i18n("unofficial.hint"));
|
||||
rootContainer.getChildren().add(unofficialHintPane);
|
||||
}
|
||||
|
||||
if (currentStep instanceof Step.Init) {
|
||||
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
loginButtonSpinner.setLoading(false);
|
||||
|
||||
var hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.setText(i18n("account.methods.microsoft.hint"));
|
||||
rootContainer.getChildren().add(hintPane);
|
||||
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
|
||||
loginButtonSpinner.setLoading(false);
|
||||
cancelAllTasks();
|
||||
|
||||
rootContainer.getChildren().add(new JFXSpinner());
|
||||
|
||||
browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE))
|
||||
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
|
||||
.executor(true);
|
||||
} else if (currentStep instanceof Step.StartDeviceCodeLogin) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
cancelAllTasks();
|
||||
|
||||
rootContainer.getChildren().add(new JFXSpinner());
|
||||
|
||||
deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE))
|
||||
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
|
||||
.executor(true);
|
||||
} else if (currentStep instanceof Step.WaitForOpenBrowser wait) {
|
||||
btnLogin.setOnAction(e -> {
|
||||
FXUtils.openLink(wait.url());
|
||||
loginButtonSpinner.setLoading(true);
|
||||
});
|
||||
loginButtonSpinner.setLoading(false);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.setSegment(
|
||||
i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()),
|
||||
FXUtils::copyText
|
||||
);
|
||||
|
||||
rootContainer.getChildren().add(hintPane);
|
||||
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
|
||||
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
|
||||
? "https://www.microsoft.com/link?otc=" + wait.userCode()
|
||||
: wait.verificationUri();
|
||||
|
||||
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
|
||||
StringUtils.escapeXmlAttribute(scanUri),
|
||||
wait.verificationUri(),
|
||||
wait.userCode()
|
||||
));
|
||||
|
||||
var qrCode = new SVGPath();
|
||||
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
|
||||
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(scanUri, QrCode.Ecc.MEDIUM)));
|
||||
qrCode.setScaleX(3);
|
||||
qrCode.setScaleY(3);
|
||||
|
||||
var lblCode = new Label(wait.userCode());
|
||||
lblCode.getStyleClass().add("code-label");
|
||||
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
|
||||
|
||||
var codeBox = new StackPane(lblCode);
|
||||
codeBox.getStyleClass().add("code-box");
|
||||
codeBox.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
|
||||
codeBox.setMaxWidth(USE_PREF_SIZE);
|
||||
|
||||
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
|
||||
} else if (currentStep instanceof Step.LoginFailed failed) {
|
||||
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
loginButtonSpinner.setLoading(false);
|
||||
cancelAllTasks();
|
||||
|
||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||
errHintPane.setSegment(failed.message());
|
||||
rootContainer.getChildren().add(errHintPane);
|
||||
}
|
||||
|
||||
var linkBox = new FlowPane(8, 8);
|
||||
linkBox.setAlignment(Pos.CENTER_LEFT);
|
||||
linkBox.setPrefWrapLength(500);
|
||||
|
||||
if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) {
|
||||
JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device"));
|
||||
useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin()));
|
||||
linkBox.getChildren().add(useQrCode);
|
||||
} else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) {
|
||||
JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser"));
|
||||
userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
linkBox.getChildren().add(userBrowser);
|
||||
}
|
||||
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
|
||||
linkBox.getChildren().addAll(profileLink, purchaseLink);
|
||||
rootContainer.getChildren().add(linkBox);
|
||||
|
||||
setBody(rootContainer);
|
||||
}
|
||||
|
||||
private void cancelAllTasks() {
|
||||
if (browserTaskExecutor != null) browserTaskExecutor.cancel();
|
||||
if (deviceTaskExecutor != null) deviceTaskExecutor.cancel();
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
cancelAllTasks();
|
||||
if (cancelCallback != null) cancelCallback.run();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
private void onLoginCompleted(MicrosoftAccount account, Exception exception) {
|
||||
if (exception == null) {
|
||||
if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin);
|
||||
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
|
||||
Accounts.setSelectedAccount(account);
|
||||
|
||||
if (loginCallback != null) {
|
||||
try {
|
||||
loginCallback.accept(account.logIn());
|
||||
} catch (AuthenticationException e) {
|
||||
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else if (!(exception instanceof CancellationException)) {
|
||||
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception)));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface Step {
|
||||
final class Init implements Step {
|
||||
}
|
||||
|
||||
final class StartAuthorizationCodeLogin implements Step {
|
||||
}
|
||||
|
||||
record WaitForOpenBrowser(String url) implements Step {
|
||||
|
||||
}
|
||||
|
||||
final class StartDeviceCodeLogin implements Step {
|
||||
}
|
||||
|
||||
record WaitForScanQrCode(String userCode, String verificationUri) implements Step {
|
||||
|
||||
}
|
||||
|
||||
record LoginFailed(String message) implements Step {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class OAuthAccountLoginDialog extends DialogPane {
|
||||
private final OAuthAccount account;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.account = account;
|
||||
this.success = success;
|
||||
this.failed = failed;
|
||||
|
||||
setTitle(i18n("account.login.refresh"));
|
||||
|
||||
VBox vbox = new VBox(8);
|
||||
Label usernameLabel = new Label(account.getUsername());
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
|
||||
);
|
||||
} else {
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.hint")
|
||||
);
|
||||
}
|
||||
});
|
||||
FXUtils.onClicked(hintPane, () -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
||||
setBody(vbox);
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
||||
}
|
||||
|
||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
||||
FXUtils.runInFX(() -> {
|
||||
deviceCode.set(event);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
||||
if (exception == null) {
|
||||
success.accept(authInfo);
|
||||
onSuccess();
|
||||
} else {
|
||||
LOG.info("Failed to login when credentials expired: " + account, exception);
|
||||
onFailure(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancel() {
|
||||
failed.run();
|
||||
super.onCancel();
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
@@ -40,6 +37,9 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -48,8 +48,8 @@ import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
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 class OfflineAccountSkinPane extends StackPane {
|
||||
private final OfflineAccount account;
|
||||
@@ -110,6 +110,7 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
|
||||
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
|
||||
cslApiField.setValidators(new URLValidator());
|
||||
FXUtils.setValidateWhileTextChanged(cslApiField, true);
|
||||
|
||||
skinItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
|
||||
@@ -218,6 +219,10 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
acceptButton.disableProperty().bind(
|
||||
skinItem.selectedDataProperty().isEqualTo(Skin.Type.CUSTOM_SKIN_LOADER_API)
|
||||
.and(cslApiField.activeValidatorProperty().isNotNull()));
|
||||
|
||||
layout.setActions(littleSkinLink, acceptButton, cancelButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -28,8 +27,8 @@ 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.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -71,10 +70,9 @@ public class AdvancedListBox extends ScrollPane {
|
||||
private AdvancedListItem createNavigationDrawerItem(String title, SVG leftGraphic) {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setActionButtonVisible(false);
|
||||
item.setTitle(title);
|
||||
if (leftGraphic != null) {
|
||||
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
|
||||
item.setLeftIcon(leftGraphic);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -107,18 +105,11 @@ public class AdvancedListBox extends ScrollPane {
|
||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||
item.setOnAction(e -> tabHeader.select(tab));
|
||||
|
||||
Node unselectedIcon = unselectedGraphic.createIcon(20);
|
||||
Node selectedIcon = selectedGraphic.createIcon(20);
|
||||
|
||||
TransitionPane leftGraphic = new TransitionPane();
|
||||
leftGraphic.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(leftGraphic, 30);
|
||||
FXUtils.setLimitHeight(leftGraphic, 20);
|
||||
leftGraphic.setPadding(Insets.EMPTY);
|
||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
||||
FXUtils.onChange(item.activeProperty(), active ->
|
||||
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
|
||||
|
||||
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||
leftGraphic.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
|
||||
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
|
||||
item.setLeftGraphic(leftGraphic);
|
||||
return add(item);
|
||||
}
|
||||
|
||||
@@ -17,105 +17,134 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class AdvancedListItem extends Control {
|
||||
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
|
||||
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
|
||||
public static final double LEFT_GRAPHIC_SIZE = 32;
|
||||
public static final double LEFT_ICON_SIZE = 20;
|
||||
public static final Insets LEFT_ICON_MARGIN = new Insets(0, 6, 0, 6);
|
||||
|
||||
public static void setMargin(Node graphic, Insets margin) {
|
||||
BorderPane.setMargin(graphic, margin);
|
||||
}
|
||||
|
||||
public static void setAlignment(Node graphic, Pos alignment) {
|
||||
BorderPane.setAlignment(graphic, alignment);
|
||||
}
|
||||
|
||||
public AdvancedListItem() {
|
||||
getStyleClass().add("advanced-list-item");
|
||||
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
|
||||
}
|
||||
|
||||
public Node getLeftGraphic() {
|
||||
return leftGraphic.get();
|
||||
}
|
||||
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
|
||||
|
||||
public ObjectProperty<Node> leftGraphicProperty() {
|
||||
return leftGraphic;
|
||||
}
|
||||
|
||||
public Node getLeftGraphic() {
|
||||
return leftGraphic.get();
|
||||
}
|
||||
|
||||
public void setLeftGraphic(Node leftGraphic) {
|
||||
this.leftGraphic.set(leftGraphic);
|
||||
}
|
||||
|
||||
public Node getRightGraphic() {
|
||||
return rightGraphic.get();
|
||||
public void setLeftIcon(SVG svg) {
|
||||
Node icon = svg.createIcon(LEFT_ICON_SIZE);
|
||||
icon.setMouseTransparent(true);
|
||||
BorderPane.setMargin(icon, LEFT_ICON_MARGIN);
|
||||
BorderPane.setAlignment(icon, Pos.CENTER);
|
||||
leftGraphicProperty().set(icon);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
|
||||
|
||||
public ObjectProperty<Node> rightGraphicProperty() {
|
||||
return rightGraphic;
|
||||
}
|
||||
|
||||
public Node getRightGraphic() {
|
||||
return rightGraphic.get();
|
||||
}
|
||||
|
||||
public void setRightGraphic(Node rightGraphic) {
|
||||
this.rightGraphic.set(rightGraphic);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
public void setRightAction(SVG icon, Runnable action) {
|
||||
var button = new JFXButton();
|
||||
button.setOnAction(e -> {
|
||||
action.run();
|
||||
e.consume();
|
||||
});
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(14));
|
||||
setAlignment(button, Pos.CENTER);
|
||||
setRightGraphic(button);
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active.get();
|
||||
}
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active.set(active);
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public boolean isActionButtonVisible() {
|
||||
return actionButtonVisible.get();
|
||||
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public BooleanProperty actionButtonVisibleProperty() {
|
||||
return actionButtonVisible;
|
||||
public boolean isActive() {
|
||||
return active.get();
|
||||
}
|
||||
|
||||
public void setActionButtonVisible(boolean actionButtonVisible) {
|
||||
this.actionButtonVisible.set(actionButtonVisible);
|
||||
public void setActive(boolean active) {
|
||||
this.active.set(active);
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<>(this, "onAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
return onAction;
|
||||
}
|
||||
@@ -128,32 +157,9 @@ public class AdvancedListItem extends Control {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new AdvancedListItemSkin(this);
|
||||
}
|
||||
|
||||
public static Pair<Node, ImageView> createImageView(Image image) {
|
||||
return createImageView(image, 32, 32);
|
||||
}
|
||||
|
||||
public static Pair<Node, ImageView> createImageView(Image image, double width, double height) {
|
||||
StackPane imageViewContainer = new StackPane();
|
||||
FXUtils.setLimitWidth(imageViewContainer, width);
|
||||
FXUtils.setLimitHeight(imageViewContainer, height);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, width, height);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(image);
|
||||
imageViewContainer.getChildren().setAll(imageView);
|
||||
return pair(imageViewContainer, imageView);
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
@@ -40,41 +38,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
|
||||
HBox left = new HBox();
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
left.setMouseTransparent(true);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
root.setCenter(item);
|
||||
item.setMouseTransparent(true);
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
|
||||
newGraphic -> {
|
||||
if (newGraphic == null) {
|
||||
left.getChildren().clear();
|
||||
} else {
|
||||
left.getChildren().setAll(newGraphic);
|
||||
}
|
||||
});
|
||||
root.setLeft(left);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER);
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
FXUtils.setLimitWidth(right, 40);
|
||||
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
|
||||
newGraphic -> {
|
||||
if (newGraphic == null) {
|
||||
right.getChildren().clear();
|
||||
} else {
|
||||
right.getChildren().setAll(newGraphic);
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
|
||||
visible -> root.setRight(visible ? right : null));
|
||||
root.leftProperty().bind(skinnable.leftGraphicProperty());
|
||||
root.rightProperty().bind(skinnable.rightGraphicProperty());
|
||||
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,11 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||
boolean noPadding = !sublist.hasComponentPadding();
|
||||
|
||||
this.getStyleClass().add("options-sublist-wrapper");
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
expandIcon.getStyleClass().add("expand-icon");
|
||||
expandIcon.setMouseTransparent(true);
|
||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
@@ -72,7 +74,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
}
|
||||
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(16);
|
||||
header.setSpacing(12);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class FileItem extends BorderPane {
|
||||
private final Label lblPath = new Label();
|
||||
|
||||
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
|
||||
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
|
||||
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
|
||||
|
||||
public FileItem() {
|
||||
VBox left = new VBox();
|
||||
Label name = new Label();
|
||||
name.textProperty().bind(nameProperty());
|
||||
lblPath.getStyleClass().addAll("subtitle-label");
|
||||
lblPath.textProperty().bind(path);
|
||||
left.getChildren().addAll(name, lblPath);
|
||||
setLeft(left);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
right.setGraphic(SVG.EDIT.createIcon(16));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
right.setOnAction(e -> onExplore());
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
setRight(right);
|
||||
|
||||
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
|
||||
*/
|
||||
private String processPath(String path) {
|
||||
Path given;
|
||||
try {
|
||||
given = Path.of(path).toAbsolutePath().normalize();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (isConvertToRelativePath()) {
|
||||
try {
|
||||
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// the given path can't be relativized against current path
|
||||
}
|
||||
}
|
||||
return given.normalize().toString();
|
||||
}
|
||||
|
||||
public void onExplore() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
if (path.get() != null) {
|
||||
Path file;
|
||||
try {
|
||||
file = Path.of(path.get());
|
||||
if (Files.exists(file)) {
|
||||
if (Files.isRegularFile(file))
|
||||
file = file.toAbsolutePath().normalize().getParent();
|
||||
else if (Files.isDirectory(file))
|
||||
file = file.toAbsolutePath().normalize();
|
||||
chooser.setInitialDirectory(file.toFile());
|
||||
}
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to resolve path: " + path.get());
|
||||
}
|
||||
}
|
||||
chooser.titleProperty().bind(titleProperty());
|
||||
var selectedDir = chooser.showDialog(Controllers.getStage());
|
||||
if (selectedDir != null) {
|
||||
path.set(processPath(selectedDir.toString()));
|
||||
}
|
||||
chooser.titleProperty().unbind();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name.get();
|
||||
}
|
||||
|
||||
public StringProperty nameProperty() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name.set(name);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path.get();
|
||||
}
|
||||
|
||||
public StringProperty pathProperty() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path.set(path);
|
||||
}
|
||||
|
||||
public boolean isConvertToRelativePath() {
|
||||
return convertToRelativePath.get();
|
||||
}
|
||||
|
||||
public BooleanProperty convertToRelativePathProperty() {
|
||||
return convertToRelativePath;
|
||||
}
|
||||
|
||||
public void setConvertToRelativePath(boolean convertToRelativePath) {
|
||||
this.convertToRelativePath.set(convertToRelativePath);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
private final Text label = new Text();
|
||||
@@ -66,7 +67,11 @@ public class HintPane extends VBox {
|
||||
}
|
||||
|
||||
public void setSegment(String segment) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
|
||||
this.setSegment(segment, Controllers::onHyperlinkAction);
|
||||
}
|
||||
|
||||
public void setSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction));
|
||||
}
|
||||
|
||||
public void setChildren(Node... children) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
public final class IconedMenuItem extends IconedItem {
|
||||
|
||||
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
|
||||
super(icon != null ? FXUtils.limitingSize(icon.createIcon(14), 14, 14) : null, text);
|
||||
super(icon != null ? icon.createIcon(14) : null, text);
|
||||
|
||||
getStyleClass().setAll("iconed-menu-item");
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class IconedTwoLineListItem extends HBox {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
|
||||
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
|
||||
private JFXButton externalLinkButton;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener observer;
|
||||
|
||||
public IconedTwoLineListItem() {
|
||||
setAlignment(Pos.CENTER);
|
||||
setSpacing(16);
|
||||
imageView.imageProperty().bind(image);
|
||||
twoLineListItem.titleProperty().bind(title);
|
||||
twoLineListItem.subtitleProperty().bind(subtitle);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
Bindings.bindContent(twoLineListItem.getTags(), tags);
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
getChildren().clear();
|
||||
if (image.get() != null) getChildren().add(imageView);
|
||||
getChildren().add(twoLineListItem);
|
||||
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
|
||||
}, image, externalLink);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public String getExternalLink() {
|
||||
return externalLink.get();
|
||||
}
|
||||
|
||||
public StringProperty externalLinkProperty() {
|
||||
return externalLink;
|
||||
}
|
||||
|
||||
public void setExternalLink(String externalLink) {
|
||||
this.externalLink.set(externalLink);
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image.set(image);
|
||||
}
|
||||
|
||||
public ImageView getImageView() {
|
||||
return imageView;
|
||||
}
|
||||
|
||||
public JFXButton getExternalLinkButton() {
|
||||
if (externalLinkButton == null) {
|
||||
externalLinkButton = new JFXButton();
|
||||
externalLinkButton.getStyleClass().add("toggle-icon4");
|
||||
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon());
|
||||
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
|
||||
}
|
||||
return externalLinkButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
/// @author Glavo
|
||||
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);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static LineButton createExternalLinkButton(String url) {
|
||||
var button = new LineButton();
|
||||
button.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
if (url != null) {
|
||||
button.setOnAction(event -> FXUtils.openLink(url));
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
public LineButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
container.setMouseTransparent(true);
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<ActionEvent>> onAction;
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
if (onAction == null) {
|
||||
onAction = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onAction";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return onAction;
|
||||
}
|
||||
|
||||
public EventHandler<ActionEvent> getOnAction() {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
public void setOnAction(EventHandler<ActionEvent> value) {
|
||||
onActionProperty().set(value);
|
||||
}
|
||||
|
||||
private StringProperty trailingText;
|
||||
|
||||
public StringProperty trailingTextProperty() {
|
||||
if (trailingText == null) {
|
||||
trailingText = new StringPropertyBase() {
|
||||
private Label trailingTextLabel;
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "trailingText";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String message = get();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
if (trailingTextLabel == null) {
|
||||
trailingTextLabel = new Label();
|
||||
trailingTextLabel.getStyleClass().add("trailing-label");
|
||||
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
}
|
||||
trailingTextLabel.setText(message);
|
||||
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
|
||||
} else if (trailingTextLabel != null) {
|
||||
trailingTextLabel.setText("");
|
||||
setNode(IDX_TRAILING_TEXT, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return trailingText;
|
||||
}
|
||||
|
||||
public String getTrailingText() {
|
||||
return trailingText != null ? trailingText.get() : null;
|
||||
}
|
||||
|
||||
public void setTrailingText(String trailingText) {
|
||||
trailingTextProperty().set(trailingText);
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> trailingIcon;
|
||||
|
||||
public ObjectProperty<Node> trailingIconProperty() {
|
||||
if (trailingIcon == null)
|
||||
trailingIcon = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "trailingIcon";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_TRAILING_ICON, get());
|
||||
}
|
||||
};
|
||||
|
||||
return trailingIcon;
|
||||
}
|
||||
|
||||
public Node getTrailingIcon() {
|
||||
return trailingIcon != null ? trailingIcon.get() : null;
|
||||
}
|
||||
|
||||
public void setTrailingIcon(Node trailingIcon) {
|
||||
trailingIconProperty().set(trailingIcon);
|
||||
}
|
||||
|
||||
public void setTrailingIcon(SVG rightIcon) {
|
||||
setTrailingIcon(rightIcon, 20);
|
||||
}
|
||||
|
||||
public void setTrailingIcon(SVG rightIcon, double size) {
|
||||
Node rightIconNode = rightIcon.createIcon(size);
|
||||
rightIconNode.getStyleClass().add("trailing-icon");
|
||||
setTrailingIcon(rightIconNode);
|
||||
}
|
||||
}
|
||||
@@ -17,107 +17,25 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.event.ActionEvent;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent {
|
||||
public abstract class LineButtonBase extends LineComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-button-base";
|
||||
|
||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||
|
||||
protected final BorderPane root;
|
||||
protected final RipplerContainer container;
|
||||
|
||||
private final Label titleLabel;
|
||||
protected final RipplerContainer ripplerContainer;
|
||||
|
||||
public LineButtonBase() {
|
||||
this.root = new BorderPane();
|
||||
root.setPadding(PADDING);
|
||||
root.setMinHeight(48);
|
||||
this.getStyleClass().addAll(LineButtonBase.DEFAULT_STYLE_CLASS);
|
||||
|
||||
this.container = new RipplerContainer(root);
|
||||
this.getChildren().setAll(container);
|
||||
this.ripplerContainer = new RipplerContainer(container);
|
||||
FXUtils.onClicked(this, this::fire);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
root.setCenter(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.textProperty().bind(titleProperty());
|
||||
titleLabel.getStyleClass().add("title");
|
||||
this.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private VBox left;
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButtonBase.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (left == null) {
|
||||
left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
root.setCenter(left);
|
||||
} else if (left != null) {
|
||||
subtitleLabel.setText(null);
|
||||
root.setCenter(titleLabel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
public void fire() {
|
||||
fireEvent(new ActionEvent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class LineComponent extends StackPane implements NoPaddingComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-component";
|
||||
private static final double MIN_HEIGHT = 48.0;
|
||||
private static final PseudoClass PSEUDO_LARGER_TITLE = PseudoClass.getPseudoClass("large-title");
|
||||
|
||||
protected static final int IDX_LEADING = 0;
|
||||
protected static final int IDX_TITLE = 1;
|
||||
protected static final int IDX_TRAILING = 2;
|
||||
|
||||
public static final double SPACING = 12;
|
||||
public static final double DEFAULT_ICON_SIZE = 20;
|
||||
|
||||
public static void setMargin(Node child, Insets value) {
|
||||
HBox.setMargin(child, value);
|
||||
}
|
||||
|
||||
protected final HBox container;
|
||||
|
||||
private final Label titleLabel;
|
||||
private final VBox titleContainer;
|
||||
|
||||
public LineComponent() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
this.setMinHeight(MIN_HEIGHT);
|
||||
|
||||
this.container = new HBox(SPACING);
|
||||
container.getStyleClass().add("line-component-container");
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
titleLabel.getStyleClass().add("title-label");
|
||||
titleLabel.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
this.titleContainer = new VBox(titleLabel);
|
||||
titleContainer.getStyleClass().add("title-container");
|
||||
titleContainer.setMouseTransparent(true);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
titleContainer.minWidthProperty().bind(titleLabel.prefWidthProperty());
|
||||
HBox.setHgrow(titleContainer, Priority.ALWAYS);
|
||||
|
||||
this.setNode(IDX_TITLE, titleContainer);
|
||||
|
||||
this.getChildren().setAll(container);
|
||||
}
|
||||
|
||||
private Node[] nodes = new Node[2];
|
||||
|
||||
protected void setNode(int idx, Node node) {
|
||||
if (nodes.length <= idx)
|
||||
nodes = Arrays.copyOf(nodes, idx + 1);
|
||||
|
||||
if (nodes[idx] != node) {
|
||||
nodes[idx] = node;
|
||||
container.getChildren().setAll(Arrays.stream(nodes).filter(Objects::nonNull).toArray(Node[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
public void setLargeTitle(boolean largeTitle) {
|
||||
pseudoClassStateChanged(PSEUDO_LARGER_TITLE, largeTitle);
|
||||
}
|
||||
|
||||
private final StringProperty title = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "title";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
titleLabel.setText(get());
|
||||
}
|
||||
};
|
||||
|
||||
public final StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public final StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (subtitleLabel == null) {
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
if (titleContainer.getChildren().size() == 1)
|
||||
titleContainer.getChildren().add(subtitleLabel);
|
||||
} else if (subtitleLabel != null) {
|
||||
subtitleLabel.setText(null);
|
||||
if (titleContainer.getChildren().size() == 2)
|
||||
titleContainer.getChildren().remove(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public final String getSubtitle() {
|
||||
return subtitle != null ? subtitle.get() : null;
|
||||
}
|
||||
|
||||
public final void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> leading;
|
||||
|
||||
public final ObjectProperty<Node> leadingProperty() {
|
||||
if (leading == null) {
|
||||
leading = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "leading";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_LEADING, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return leading;
|
||||
}
|
||||
|
||||
public final Node getLeading() {
|
||||
return leadingProperty().get();
|
||||
}
|
||||
|
||||
public final void setLeading(Node node) {
|
||||
leadingProperty().set(node);
|
||||
}
|
||||
|
||||
public void setLeading(Image icon) {
|
||||
setLeading(icon, -1);
|
||||
}
|
||||
|
||||
public void setLeading(Image icon, double size) {
|
||||
var imageView = new ImageView(icon);
|
||||
if (size > 0) {
|
||||
imageView.setFitWidth(size);
|
||||
imageView.setFitHeight(size);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setSmooth(true);
|
||||
}
|
||||
imageView.setMouseTransparent(true);
|
||||
|
||||
setNode(IDX_LEADING, imageView);
|
||||
}
|
||||
|
||||
public void setLeading(SVG svg) {
|
||||
setLeading(svg, DEFAULT_ICON_SIZE);
|
||||
}
|
||||
|
||||
public void setLeading(SVG svg, double size) {
|
||||
Node node = svg.createIcon(size);
|
||||
node.setMouseTransparent(true);
|
||||
setNode(IDX_LEADING, node);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class LineFileChooserButton extends LineButton {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-file-select-button";
|
||||
|
||||
public LineFileChooserButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setTrailingIcon(SVG.EDIT);
|
||||
}
|
||||
|
||||
/// Converts the given path to absolute/relative(if possible) path according to [#convertToRelativePathProperty()].
|
||||
private String processPath(Path path) {
|
||||
if (isConvertToRelativePath() && path.isAbsolute()) {
|
||||
try {
|
||||
return Metadata.CURRENT_DIRECTORY.relativize(path).normalize().toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// the given path can't be relativized against current path
|
||||
}
|
||||
}
|
||||
return path.normalize().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire() {
|
||||
super.fire();
|
||||
|
||||
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
|
||||
String windowTitle = getFileChooserTitle();
|
||||
|
||||
Path initialDirectory = null;
|
||||
if (getLocation() != null) {
|
||||
Path file;
|
||||
try {
|
||||
file = FileUtils.toAbsolute(Path.of(getLocation()));
|
||||
if (Files.exists(file)) {
|
||||
if (Files.isRegularFile(file))
|
||||
initialDirectory = file.getParent();
|
||||
else if (Files.isDirectory(file))
|
||||
initialDirectory = file;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Failed to resolve path: " + getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
Path path;
|
||||
Type type = getType();
|
||||
if (type == Type.OPEN_DIRECTORY) {
|
||||
var directoryChooser = new DirectoryChooser();
|
||||
if (windowTitle != null)
|
||||
directoryChooser.setTitle(windowTitle);
|
||||
if (initialDirectory != null)
|
||||
directoryChooser.setInitialDirectory(initialDirectory.toFile());
|
||||
|
||||
path = FileUtils.toPath(directoryChooser.showDialog(owner));
|
||||
} else {
|
||||
var fileChooser = new FileChooser();
|
||||
if (windowTitle != null)
|
||||
fileChooser.setTitle(windowTitle);
|
||||
if (initialDirectory != null)
|
||||
fileChooser.setInitialDirectory(initialDirectory.toFile());
|
||||
|
||||
if (extensionFilters != null)
|
||||
fileChooser.getExtensionFilters().setAll(extensionFilters);
|
||||
|
||||
fileChooser.setInitialFileName(getInitialFileName());
|
||||
|
||||
path = FileUtils.toPath(switch (type) {
|
||||
case OPEN_FILE -> fileChooser.showOpenDialog(owner);
|
||||
case SAVE_FILE -> fileChooser.showSaveDialog(owner);
|
||||
default -> throw new AssertionError("Unknown Type: " + type);
|
||||
});
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
setLocation(processPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
private final StringProperty location = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineFileChooserButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "location";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setTrailingText(get());
|
||||
}
|
||||
};
|
||||
|
||||
public StringProperty locationProperty() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return locationProperty().get();
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
locationProperty().set(location);
|
||||
}
|
||||
|
||||
private final StringProperty fileChooserTitle = new SimpleStringProperty(this, "fileChooserTitle");
|
||||
|
||||
public StringProperty fileChooserTitleProperty() {
|
||||
return fileChooserTitle;
|
||||
}
|
||||
|
||||
public String getFileChooserTitle() {
|
||||
return fileChooserTitleProperty().get();
|
||||
}
|
||||
|
||||
public void setFileChooserTitle(String fileChooserTitle) {
|
||||
fileChooserTitleProperty().set(fileChooserTitle);
|
||||
}
|
||||
|
||||
private ObjectProperty<Type> type;
|
||||
|
||||
public ObjectProperty<Type> typeProperty() {
|
||||
if (type == null) {
|
||||
type = new SimpleObjectProperty<>(this, "type", Type.OPEN_FILE);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type != null ? type.get() : Type.OPEN_FILE;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
typeProperty().set(type);
|
||||
}
|
||||
|
||||
private ObjectProperty<String> initialFileName;
|
||||
|
||||
public final ObjectProperty<String> initialFileNameProperty() {
|
||||
if (initialFileName == null)
|
||||
initialFileName = new SimpleObjectProperty<>(this, "initialFileName");
|
||||
|
||||
return initialFileName;
|
||||
}
|
||||
|
||||
public final String getInitialFileName() {
|
||||
return initialFileName != null ? initialFileName.get() : null;
|
||||
}
|
||||
|
||||
public final void setInitialFileName(String value) {
|
||||
initialFileNameProperty().set(value);
|
||||
}
|
||||
|
||||
private ObservableList<FileChooser.ExtensionFilter> extensionFilters;
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
if (extensionFilters == null)
|
||||
extensionFilters = FXCollections.observableArrayList();
|
||||
return extensionFilters;
|
||||
}
|
||||
|
||||
private BooleanProperty convertToRelativePath;
|
||||
|
||||
public BooleanProperty convertToRelativePathProperty() {
|
||||
if (convertToRelativePath == null)
|
||||
convertToRelativePath = new BooleanPropertyBase(false) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineFileChooserButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "convertToRelativePath";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String location = getLocation();
|
||||
if (location == null)
|
||||
return;
|
||||
try {
|
||||
setLocation(processPath(FileUtils.toAbsolute(Path.of(getLocation()))));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
};
|
||||
return convertToRelativePath;
|
||||
}
|
||||
|
||||
public boolean isConvertToRelativePath() {
|
||||
return convertToRelativePath != null && convertToRelativePath.get();
|
||||
}
|
||||
|
||||
public void setConvertToRelativePath(boolean convertToRelativePath) {
|
||||
convertToRelativePathProperty().set(convertToRelativePath);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
OPEN_FILE,
|
||||
OPEN_DIRECTORY,
|
||||
SAVE_FILE
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineNavigationButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-navigation-button";
|
||||
|
||||
public LineNavigationButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
root.setMouseTransparent(true);
|
||||
|
||||
HBox right = new HBox();
|
||||
root.setRight(right);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Label valueLabel = new Label();
|
||||
valueLabel.getStyleClass().add("subtitle");
|
||||
valueLabel.textProperty().bind(messageProperty());
|
||||
|
||||
Node arrowIcon = SVG.ARROW_FORWARD.createIcon(24);
|
||||
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
disabledProperty().addListener((observable, oldValue, newValue) ->
|
||||
arrowIcon.setOpacity(newValue ? 0.4 : 1.0));
|
||||
|
||||
right.getChildren().setAll(valueLabel, arrowIcon);
|
||||
}
|
||||
|
||||
FXUtils.onClicked(container, this::fire);
|
||||
}
|
||||
|
||||
public void fire() {
|
||||
fireEvent(new ActionEvent());
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<>() {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineNavigationButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onAction";
|
||||
}
|
||||
};
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
return onAction;
|
||||
}
|
||||
|
||||
public EventHandler<ActionEvent> getOnAction() {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
public void setOnAction(EventHandler<ActionEvent> value) {
|
||||
onActionProperty().set(value);
|
||||
}
|
||||
|
||||
private final StringProperty message = new SimpleStringProperty(this, "message", "");
|
||||
|
||||
public StringProperty messageProperty() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return messageProperty().get();
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
messageProperty().set(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,99 +17,47 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.scene.Node;
|
||||
|
||||
/// @author Glavo
|
||||
public class LinePane extends BorderPane implements NoPaddingComponent {
|
||||
|
||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||
|
||||
private final Label titleLabel;
|
||||
public class LinePane extends LineComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-pane";
|
||||
|
||||
public LinePane() {
|
||||
this.setPadding(PADDING);
|
||||
this.setMinHeight(48);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
this.setCenter(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.textProperty().bind(titleProperty());
|
||||
titleLabel.getStyleClass().add("title");
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private VBox left;
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
private ObjectProperty<Node> right;
|
||||
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
if (right == null) {
|
||||
right = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LinePane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (left == null) {
|
||||
left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
public String getName() {
|
||||
return "right";
|
||||
}
|
||||
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
LinePane.this.setCenter(left);
|
||||
} else if (left != null) {
|
||||
subtitleLabel.setText(null);
|
||||
LinePane.this.setCenter(titleLabel);
|
||||
}
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_TRAILING, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
return right;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
public Node getRight() {
|
||||
return rightProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
public void setRight(Node right) {
|
||||
rightProperty().set(right);
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,7 @@ import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -43,7 +41,7 @@ import java.util.function.Function;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineSelectButton<T> extends LineButtonBase {
|
||||
public final 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");
|
||||
@@ -53,109 +51,97 @@ public final class LineSelectButton<T> extends LineButtonBase {
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
root.setMouseTransparent(true);
|
||||
InvalidationListener updateTrailingText = observable -> {
|
||||
T value = getValue();
|
||||
if (value != null) {
|
||||
Function<T, String> converter = getConverter();
|
||||
setTrailingText(converter != null ? converter.apply(value) : value.toString());
|
||||
} else {
|
||||
setTrailingText(null);
|
||||
}
|
||||
};
|
||||
converterProperty().addListener(updateTrailingText);
|
||||
valueProperty().addListener(updateTrailingText);
|
||||
|
||||
HBox right = new HBox();
|
||||
root.setRight(right);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
setTrailingIcon(SVG.UNFOLD_MORE);
|
||||
|
||||
Label valueLabel = new Label();
|
||||
valueLabel.getStyleClass().add("subtitle");
|
||||
|
||||
InvalidationListener updateValue = observable -> {
|
||||
T value = getValue();
|
||||
if (value == null)
|
||||
valueLabel.setText("");
|
||||
else {
|
||||
Function<T, String> converter = getConverter();
|
||||
valueLabel.setText(converter != null ? converter.apply(value) : value.toString());
|
||||
}
|
||||
};
|
||||
converterProperty().addListener(updateValue);
|
||||
valueProperty().addListener(updateValue);
|
||||
|
||||
Node arrowIcon = SVG.UNFOLD_MORE.createIcon(24);
|
||||
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
right.getChildren().setAll(valueLabel, arrowIcon);
|
||||
}
|
||||
|
||||
container.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (popup == null) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
this.popup = new JFXPopup(popupMenu);
|
||||
|
||||
container.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
itemTitleLabel.getStyleClass().add("title");
|
||||
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (item == null)
|
||||
return "";
|
||||
|
||||
Function<T, String> converter = getConverter();
|
||||
return converter != null ? converter.apply(item) : Objects.toString(item, "");
|
||||
}, converterProperty()));
|
||||
|
||||
var itemSubtitleLabel = new Label();
|
||||
itemSubtitleLabel.getStyleClass().add("subtitle");
|
||||
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
Function<T, String> descriptionConverter = getDescriptionConverter();
|
||||
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
|
||||
}, descriptionConverterProperty()));
|
||||
|
||||
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
|
||||
if (text == null || text.isEmpty()) {
|
||||
vbox.getChildren().setAll(itemTitleLabel);
|
||||
} else {
|
||||
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
|
||||
}
|
||||
});
|
||||
|
||||
var wrapper = new StackPane(vbox);
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("menu-container");
|
||||
wrapper.setMouseTransparent(true);
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
|
||||
FXUtils.onClicked(ripplerContainer, () -> {
|
||||
setValue(item);
|
||||
popup.hide();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(valueProperty(),
|
||||
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
|
||||
|
||||
return ripplerContainer;
|
||||
}));
|
||||
|
||||
popup.showingProperty().addListener((observable, oldValue, newValue) ->
|
||||
container.getRippler().setRipplerDisabled(newValue));
|
||||
}
|
||||
|
||||
if (popup.isShowing()) {
|
||||
popup.hide();
|
||||
} else {
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
|
||||
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight());
|
||||
}
|
||||
|
||||
event.consume();
|
||||
} else if (event.getButton() == MouseButton.SECONDARY) {
|
||||
ripplerContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
if (popup != null)
|
||||
popup.hide();
|
||||
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire() {
|
||||
super.fire();
|
||||
if (popup == null) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
this.popup = new JFXPopup(popupMenu);
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
itemTitleLabel.getStyleClass().add("title-label");
|
||||
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (item == null)
|
||||
return "";
|
||||
|
||||
Function<T, String> converter = getConverter();
|
||||
return converter != null ? converter.apply(item) : Objects.toString(item, "");
|
||||
}, converterProperty()));
|
||||
|
||||
var itemSubtitleLabel = new Label();
|
||||
itemSubtitleLabel.getStyleClass().add("subtitle-label");
|
||||
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
Function<T, String> descriptionConverter = getDescriptionConverter();
|
||||
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
|
||||
}, descriptionConverterProperty()));
|
||||
|
||||
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
|
||||
if (text == null || text.isEmpty()) {
|
||||
vbox.getChildren().setAll(itemTitleLabel);
|
||||
} else {
|
||||
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
|
||||
}
|
||||
});
|
||||
|
||||
var wrapper = new StackPane(vbox);
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("menu-container");
|
||||
wrapper.setMouseTransparent(true);
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
|
||||
FXUtils.onClicked(ripplerContainer, () -> {
|
||||
setValue(item);
|
||||
popup.hide();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(valueProperty(),
|
||||
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
|
||||
|
||||
return ripplerContainer;
|
||||
}));
|
||||
|
||||
popup.showingProperty().addListener((observable, oldValue, newValue) ->
|
||||
ripplerContainer.getRippler().setRipplerDisabled(newValue));
|
||||
}
|
||||
|
||||
if (popup.isShowing()) {
|
||||
popup.hide();
|
||||
} else {
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
|
||||
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
private final ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
|
||||
|
||||
public ObjectProperty<T> valueProperty() {
|
||||
@@ -187,9 +173,8 @@ public final class LineSelectButton<T> extends LineButtonBase {
|
||||
private ObjectProperty<Function<T, String>> descriptionConverter;
|
||||
|
||||
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
|
||||
if (descriptionConverter == null) {
|
||||
if (descriptionConverter == null)
|
||||
descriptionConverter = new SimpleObjectProperty<>(this, "descriptionConverter");
|
||||
}
|
||||
return descriptionConverter;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,19 +19,16 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineTextPane extends LinePane {
|
||||
public final class LineTextPane extends LineComponent {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-label-pane";
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-text-pane";
|
||||
|
||||
public LineTextPane() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.getStyleClass().addAll(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
private StringProperty text;
|
||||
@@ -39,8 +36,6 @@ public final class LineTextPane extends LinePane {
|
||||
public StringProperty textProperty() {
|
||||
if (text == null) {
|
||||
text = new StringPropertyBase() {
|
||||
private static final Insets LABEL_MARGIN = new Insets(0, 8, 0, 16);
|
||||
|
||||
private Label rightLabel;
|
||||
|
||||
@Override
|
||||
@@ -56,19 +51,18 @@ public final class LineTextPane extends LinePane {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String text = get();
|
||||
if (text == null || text.isEmpty()) {
|
||||
if (rightLabel != null)
|
||||
rightLabel.setText(null);
|
||||
LineTextPane.this.setRight(null);
|
||||
} else {
|
||||
if (text != null && !text.isEmpty()) {
|
||||
if (rightLabel == null) {
|
||||
rightLabel = FXUtils.newSafeTruncatedLabel();
|
||||
FXUtils.copyOnDoubleClick(rightLabel);
|
||||
BorderPane.setMargin(rightLabel, LABEL_MARGIN);
|
||||
BorderPane.setAlignment(rightLabel, Pos.CENTER_RIGHT);
|
||||
}
|
||||
rightLabel.setText(text);
|
||||
LineTextPane.this.setRight(rightLabel);
|
||||
setNode(IDX_TRAILING, rightLabel);
|
||||
} else {
|
||||
if (rightLabel != null)
|
||||
rightLabel.setText(null);
|
||||
|
||||
setNode(IDX_TRAILING, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,25 +20,27 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class LineToggleButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-toggle-button";
|
||||
|
||||
private final JFXToggleButton toggleButton;
|
||||
|
||||
public LineToggleButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
this.toggleButton = new JFXToggleButton();
|
||||
toggleButton.selectedProperty().bindBidirectional(selectedProperty());
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
setNode(IDX_TRAILING, toggleButton);
|
||||
}
|
||||
|
||||
BorderPane.setAlignment(toggleButton, Pos.CENTER);
|
||||
root.setRight(toggleButton);
|
||||
|
||||
FXUtils.onClicked(container, toggleButton::fire);
|
||||
@Override
|
||||
public void fire() {
|
||||
toggleButton.fire();
|
||||
super.fire();
|
||||
}
|
||||
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
@@ -31,16 +31,12 @@ import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MessageDialogPane extends HBox {
|
||||
@@ -72,7 +68,7 @@ public final class MessageDialogPane extends HBox {
|
||||
private @Nullable ButtonBase cancelButton;
|
||||
|
||||
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
|
||||
this.setSpacing(8);
|
||||
this.setSpacing(16);
|
||||
this.getStyleClass().add("jfx-dialog-layout");
|
||||
|
||||
Label graphic = new Label();
|
||||
@@ -219,36 +215,6 @@ public final class MessageDialogPane extends HBox {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder cancelOnTimeout(long timeoutMs) {
|
||||
if (dialog.getCancelButton() == null) {
|
||||
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
|
||||
}
|
||||
|
||||
ButtonBase cancelButton = dialog.getCancelButton();
|
||||
String originalText = cancelButton.getText();
|
||||
|
||||
Timer timer = Lang.getTimer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
long timeout = timeoutMs;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (timeout <= 0) {
|
||||
cancel();
|
||||
runInFX(() -> {
|
||||
cancelButton.fire();
|
||||
});
|
||||
return;
|
||||
}
|
||||
timeout -= 1000;
|
||||
long currentTimeout = timeout;
|
||||
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
|
||||
}
|
||||
}, 1000, 1000);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageDialogPane build() {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
@@ -33,14 +31,12 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
|
||||
@DefaultProperty("content")
|
||||
/// A spinner pane that can show spinner, failed reason, or content.
|
||||
public class SpinnerPane extends Control {
|
||||
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
|
||||
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
|
||||
private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
|
||||
|
||||
public SpinnerPane() {
|
||||
getStyleClass().add("spinner-pane");
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
public void showSpinner() {
|
||||
@@ -52,108 +48,195 @@ public class SpinnerPane extends Control {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
return content.get();
|
||||
private void updateContent() {
|
||||
if (getSkin() instanceof Skin skin) {
|
||||
skin.updateContent();
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> content;
|
||||
|
||||
public ObjectProperty<Node> contentProperty() {
|
||||
if (content == null)
|
||||
content = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SpinnerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "content";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
updateContent();
|
||||
}
|
||||
};
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(Node content) {
|
||||
this.content.set(content);
|
||||
public Node getContent() {
|
||||
return contentProperty().get();
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
public void setContent(Node content) {
|
||||
contentProperty().set(content);
|
||||
}
|
||||
|
||||
private BooleanProperty loading;
|
||||
|
||||
public BooleanProperty loadingProperty() {
|
||||
if (loading == null)
|
||||
loading = new BooleanPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SpinnerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "loading";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
updateContent();
|
||||
}
|
||||
};
|
||||
return loading;
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
public boolean isLoading() {
|
||||
return loading != null && loading.get();
|
||||
}
|
||||
|
||||
public String getFailedReason() {
|
||||
return failedReason.get();
|
||||
public void setLoading(boolean loading) {
|
||||
loadingProperty().set(loading);
|
||||
}
|
||||
|
||||
private StringProperty failedReason;
|
||||
|
||||
public StringProperty failedReasonProperty() {
|
||||
if (failedReason == null)
|
||||
failedReason = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SpinnerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "failedReason";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
updateContent();
|
||||
}
|
||||
};
|
||||
return failedReason;
|
||||
}
|
||||
|
||||
public void setFailedReason(String failedReason) {
|
||||
this.failedReason.set(failedReason);
|
||||
public String getFailedReason() {
|
||||
return failedReason != null ? failedReason.get() : null;
|
||||
}
|
||||
|
||||
public void setFailedReason(String failedReason) {
|
||||
failedReasonProperty().set(failedReason);
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<Event>> onFailedAction;
|
||||
|
||||
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
|
||||
if (onFailedAction == null) {
|
||||
onFailedAction = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SpinnerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onFailedAction";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return onFailedAction;
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedAction != null ? onFailedAction.get() : null;
|
||||
}
|
||||
|
||||
public final void setOnFailedAction(EventHandler<Event> value) {
|
||||
onFailedActionProperty().set(value);
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedActionProperty().get();
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected SkinBase<SpinnerPane> createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
private static final class Skin extends SkinBase<SpinnerPane> {
|
||||
private final JFXSpinner spinner = new JFXSpinner();
|
||||
private final StackPane contentPane = new StackPane();
|
||||
private final StackPane topPane = new StackPane();
|
||||
private final TransitionPane root = new TransitionPane();
|
||||
private final StackPane failedPane = new StackPane();
|
||||
private final Label failedReasonLabel = new Label();
|
||||
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
|
||||
private final InvalidationListener observer;
|
||||
|
||||
Skin(SpinnerPane control) {
|
||||
super(control);
|
||||
|
||||
topPane.getChildren().setAll(spinner);
|
||||
topPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getChildren().setAll(failedReasonLabel);
|
||||
FXUtils.onClicked(failedPane, () -> {
|
||||
EventHandler<Event> action = control.getOnFailedAction();
|
||||
if (action != null)
|
||||
action.handle(new Event(FAILED_ACTION));
|
||||
});
|
||||
updateContent();
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
|
||||
if (newValue == null) {
|
||||
private StackPane contentPane;
|
||||
private StackPane spinnerPane;
|
||||
private StackPane failedPane;
|
||||
private Label failedReasonLabel;
|
||||
|
||||
void updateContent() {
|
||||
SpinnerPane control = getSkinnable();
|
||||
|
||||
Node nextContent;
|
||||
if (control.isLoading()) {
|
||||
if (spinnerPane == null) {
|
||||
spinnerPane = new StackPane(new JFXSpinner());
|
||||
spinnerPane.getStyleClass().add("notice-pane");
|
||||
}
|
||||
nextContent = spinnerPane;
|
||||
} else if (control.getFailedReason() != null) {
|
||||
if (failedPane == null) {
|
||||
failedReasonLabel = new Label();
|
||||
failedPane = new StackPane(failedReasonLabel);
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
|
||||
}
|
||||
failedReasonLabel.setText(control.getFailedReason());
|
||||
nextContent = failedPane;
|
||||
} else {
|
||||
if (contentPane == null) {
|
||||
contentPane = new StackPane();
|
||||
}
|
||||
|
||||
Node content = control.getContent();
|
||||
if (content != null)
|
||||
contentPane.getChildren().setAll(content);
|
||||
else
|
||||
contentPane.getChildren().clear();
|
||||
} else {
|
||||
contentPane.getChildren().setAll(newValue);
|
||||
}
|
||||
});
|
||||
getChildren().setAll(root);
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
if (getSkinnable().getFailedReason() != null) {
|
||||
root.setContent(failedPane, ContainerAnimations.FADE);
|
||||
failedReasonLabel.setText(getSkinnable().getFailedReason());
|
||||
} else if (getSkinnable().isLoading()) {
|
||||
root.setContent(topPane, ContainerAnimations.FADE);
|
||||
} else {
|
||||
root.setContent(contentPane, ContainerAnimations.FADE);
|
||||
}
|
||||
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
|
||||
nextContent = contentPane;
|
||||
}
|
||||
|
||||
if (nextContent != failedPane && failedReasonLabel != null) {
|
||||
failedReasonLabel.setText(null);
|
||||
}
|
||||
|
||||
root.setContent(nextContent, ContainerAnimations.FADE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,126 +18,185 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private static Label createTagLabel(String tag) {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
}
|
||||
private final HBox firstLine;
|
||||
private HBox secondLine;
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final Label lblSubtitle;
|
||||
private final Label lblTitle;
|
||||
private Label lblSubtitle;
|
||||
|
||||
private final AggregatedObservableList<Node> firstLineChildren;
|
||||
public TwoLineListItem() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setMouseTransparent(true);
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
|
||||
this.firstLine = new HBox(lblTitle);
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
firstLine.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.getChildren().setAll(firstLine);
|
||||
}
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
this();
|
||||
|
||||
title.set(titleString);
|
||||
subtitle.set(subtitleString);
|
||||
setTitle(titleString);
|
||||
setSubtitle(subtitleString);
|
||||
}
|
||||
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
private void initSecondLine() {
|
||||
if (secondLine == null) {
|
||||
lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
secondLine = new HBox(lblSubtitle);
|
||||
}
|
||||
}
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
private final StringProperty title = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return TwoLineListItem.this;
|
||||
}
|
||||
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
@Override
|
||||
public String getName() {
|
||||
return "title";
|
||||
}
|
||||
|
||||
lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
lblTitle.setText(get());
|
||||
}
|
||||
};
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
|
||||
getChildren().setAll(firstLine, secondLine);
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitle, subtitleString -> {
|
||||
if (subtitleString == null) getChildren().setAll(firstLine);
|
||||
else getChildren().setAll(firstLine, secondLine);
|
||||
});
|
||||
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return TwoLineListItem.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
|
||||
if (subtitle != null) {
|
||||
initSecondLine();
|
||||
lblSubtitle.setText(subtitle);
|
||||
|
||||
if (getChildren().size() == 1)
|
||||
getChildren().add(secondLine);
|
||||
} else if (secondLine != null) {
|
||||
lblSubtitle.setText(null);
|
||||
if (getChildren().size() > 1)
|
||||
getChildren().setAll(firstLine);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
public String getSubtitle() {
|
||||
return subtitle != null ? subtitleProperty().get() : null;
|
||||
}
|
||||
|
||||
public Label getSubtitleLabel() {
|
||||
return lblSubtitle;
|
||||
public void setSubtitle(String subtitle) {
|
||||
if (this.subtitle == null && subtitle == null)
|
||||
return;
|
||||
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
getTags().add(tagLabel);
|
||||
public Label getSubtitleLabel() {
|
||||
initSecondLine();
|
||||
return lblSubtitle;
|
||||
}
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag-warning");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
private ObservableList<Label> tags;
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
if (tags == null) {
|
||||
tags = FXCollections.observableArrayList();
|
||||
|
||||
var tagsBox = new HBox(8);
|
||||
tagsBox.getStyleClass().add("tags");
|
||||
tagsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
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);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void addTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
addTag(tag, null);
|
||||
}
|
||||
|
||||
private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
addTag(tag, WARNING_PSEUDO_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
return "TwoLineListItem[title=%s, subtitle=%s, tags=%s]".formatted(getTitle(), getSubtitle(), tags);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import com.jfoenix.controls.JFXSnackbarLayout;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
@@ -454,7 +455,7 @@ public class DecoratorController {
|
||||
// ==== Toast ====
|
||||
|
||||
public void showToast(String content) {
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(content, null, 2000L, false, null));
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content)));
|
||||
}
|
||||
|
||||
// ==== Wizard ====
|
||||
|
||||
@@ -239,7 +239,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
btnMin.setFocusTraversable(false);
|
||||
btnMin.setGraphic(SVG.MINIMIZE.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.getStyleClass().add("jfx-decorator-button");
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
hideSpinner();
|
||||
lblName.setText(FileUtils.getName(selectedFile));
|
||||
nameProperty.set(FileUtils.getName(selectedFile));
|
||||
installAsVersion.set(false);
|
||||
|
||||
if (name == null) {
|
||||
@@ -127,9 +127,9 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
} else {
|
||||
hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
nameProperty.set(manifest.getName());
|
||||
versionProperty.set(manifest.getVersion());
|
||||
authorProperty.set(manifest.getAuthor());
|
||||
|
||||
if (name == null) {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LinePane;
|
||||
import org.jackhuang.hmcl.ui.construct.LineTextPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
@@ -23,9 +24,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
protected final WizardController controller;
|
||||
|
||||
protected final Label lblName;
|
||||
protected final Label lblVersion;
|
||||
protected final Label lblAuthor;
|
||||
protected final StringProperty nameProperty;
|
||||
protected final StringProperty versionProperty;
|
||||
protected final StringProperty authorProperty;
|
||||
protected final JFXTextField txtModpackName;
|
||||
protected final JFXButton btnInstall;
|
||||
protected final JFXButton btnDescription;
|
||||
@@ -33,54 +34,43 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
protected ModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
this.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
VBox borderPane = new VBox();
|
||||
borderPane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(borderPane, 500);
|
||||
|
||||
ComponentList componentList = new ComponentList();
|
||||
{
|
||||
BorderPane archiveNamePane = new BorderPane();
|
||||
var archiveNamePane = new LinePane();
|
||||
{
|
||||
Label label = new Label(i18n("archive.file.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
archiveNamePane.setLeft(label);
|
||||
archiveNamePane.setTitle(i18n("archive.file.name"));
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
txtModpackName.setPrefWidth(300);
|
||||
// FIXME: Validator are not shown properly
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setCenter(txtModpackName);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
}
|
||||
|
||||
BorderPane modpackNamePane = new BorderPane();
|
||||
var modpackNamePane = new LineTextPane();
|
||||
{
|
||||
modpackNamePane.setLeft(new Label(i18n("modpack.name")));
|
||||
|
||||
lblName = new Label();
|
||||
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
|
||||
modpackNamePane.setCenter(lblName);
|
||||
modpackNamePane.setTitle(i18n("modpack.name"));
|
||||
nameProperty = modpackNamePane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane versionPane = new BorderPane();
|
||||
var versionPane = new LineTextPane();
|
||||
{
|
||||
versionPane.setLeft(new Label(i18n("archive.version")));
|
||||
|
||||
lblVersion = new Label();
|
||||
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
|
||||
versionPane.setCenter(lblVersion);
|
||||
versionPane.setTitle(i18n("archive.version"));
|
||||
versionProperty = versionPane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane authorPane = new BorderPane();
|
||||
var authorPane = new LineTextPane();
|
||||
{
|
||||
authorPane.setLeft(new Label(i18n("archive.author")));
|
||||
|
||||
lblAuthor = new Label();
|
||||
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
|
||||
authorPane.setCenter(lblAuthor);
|
||||
authorPane.setTitle(i18n("archive.author"));
|
||||
authorProperty = authorPane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane descriptionPane = new BorderPane();
|
||||
var descriptionPane = new BorderPane();
|
||||
{
|
||||
btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
|
||||
@@ -139,7 +139,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
} else {
|
||||
handler.reject(e.getMessage());
|
||||
}
|
||||
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
// otherwise we still consider the file as modpack zip file
|
||||
// since casually the url may not ends with ".zip"
|
||||
@@ -156,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
} else {
|
||||
handler.reject(e.getMessage());
|
||||
}
|
||||
}).executor(true),
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NORMAL
|
||||
);
|
||||
|
||||
@@ -53,9 +53,9 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
return;
|
||||
}
|
||||
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
nameProperty.set(manifest.getName());
|
||||
versionProperty.set(manifest.getVersion());
|
||||
authorProperty.set(manifest.getAuthor());
|
||||
|
||||
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
|
||||
@@ -60,7 +60,6 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -131,11 +130,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(libraryId);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
refresh();
|
||||
}
|
||||
@@ -168,6 +162,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
RemoteVersionListCell(VersionsPage control) {
|
||||
this.control = control;
|
||||
|
||||
imageView.setMouseTransparent(true);
|
||||
|
||||
HBox hbox = new HBox(16);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import com.jfoenix.controls.JFXTextArea;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -53,8 +57,6 @@ import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
@@ -127,7 +129,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
exportInfo.setVersion(version.get());
|
||||
exportInfo.setAuthor(author.get());
|
||||
exportInfo.setDescription(description.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setUrl(url.get());
|
||||
exportInfo.setForceUpdate(forceUpdate.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
@@ -171,10 +172,11 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
|
||||
private ObservableList<Node> originList;
|
||||
|
||||
private final List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
Insets insets = new Insets(5, 0, 12, 0);
|
||||
Insets componentListMargin = new Insets(16, 0, 16, 0);
|
||||
|
||||
ScrollPane scroll = new ScrollPane();
|
||||
@@ -182,7 +184,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
scroll.setFitToHeight(true);
|
||||
getChildren().setAll(scroll);
|
||||
|
||||
List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
@@ -209,83 +210,52 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
BorderPane.setMargin(list, componentListMargin);
|
||||
borderPane.setCenter(list);
|
||||
|
||||
var instanceNamePane = new LineTextPane();
|
||||
{
|
||||
BorderPane borderPane1 = new BorderPane();
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
|
||||
instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
|
||||
instanceNamePane.setText(skinnable.versionName);
|
||||
|
||||
Label versionNameLabel = new Label();
|
||||
versionNameLabel.setText(skinnable.versionName);
|
||||
borderPane1.setRight(versionNameLabel);
|
||||
list.getContent().add(borderPane1);
|
||||
list.getContent().add(instanceNamePane);
|
||||
}
|
||||
|
||||
{
|
||||
GridPane pane = new GridPane();
|
||||
list.getContent().add(pane);
|
||||
pane.setHgap(16);
|
||||
pane.setVgap(8);
|
||||
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
|
||||
int rowIndex = 0;
|
||||
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())
|
||||
);
|
||||
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackName);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName);
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.file_api"), skinnable.fileApi,
|
||||
skinnable.options.isValidateFileApi() ? new RequiredValidator() : null,
|
||||
new URLValidator(true)
|
||||
));
|
||||
}
|
||||
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor);
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("settings.advanced.minecraft_arguments"), skinnable.launchArguments
|
||||
));
|
||||
}
|
||||
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackVersion);
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion);
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("settings.advanced.jvm_args"), skinnable.javaArguments
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new URLValidator(true));
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
validatingFields.add(txtMcbbs);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
|
||||
}
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.origin.url"), skinnable.url
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.origin.mcbbs"), skinnable.mcbbsThreadId,
|
||||
new NumberValidator(i18n("input.number"), true)
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
@@ -339,78 +309,51 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
|
||||
cboServers.setMaxWidth(Double.MAX_VALUE);
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
|
||||
serversSelectButton.setTitle(i18n("account.injector.server"));
|
||||
serversSelectButton.setConverter(AuthlibInjectorServer::getName);
|
||||
serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
|
||||
serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
|
||||
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
|
||||
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
|
||||
.map(AuthlibInjectorServer::getUrl)
|
||||
.orElse(null)));
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> {
|
||||
AuthlibInjectorServer selected = serversSelectButton.getValue();
|
||||
return selected != null ? selected.getUrl() : null;
|
||||
}, serversSelectButton.valueProperty()));
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("account.injector.server"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(cboServers);
|
||||
|
||||
list.getContent().add(pane);
|
||||
list.getContent().add(serversSelectButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
list.getContent().add(pane);
|
||||
var requireForceUpdateButton = new LineToggleButton();
|
||||
requireForceUpdateButton.setTitle(i18n("modpack.wizard.step.initialization.force_update"));
|
||||
requireForceUpdateButton.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
list.getContent().add(requireForceUpdateButton);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(pane);
|
||||
var canIncludeLauncherButton = new LineToggleButton();
|
||||
canIncludeLauncherButton.setTitle(i18n("modpack.wizard.step.initialization.include_launcher"));
|
||||
canIncludeLauncherButton.setDisable(!skinnable.canIncludeLauncher);
|
||||
canIncludeLauncherButton.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
list.getContent().add(canIncludeLauncherButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
|
||||
BorderPane noCreateRemoteFiles = new BorderPane();
|
||||
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
|
||||
list.getContent().add(noCreateRemoteFiles);
|
||||
var requireNoCreateRemoteFilesButton = new LineToggleButton();
|
||||
requireNoCreateRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.no_create_remote_files"));
|
||||
requireNoCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
|
||||
|
||||
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton();
|
||||
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
|
||||
noCreateRemoteFilesButton.setSize(8);
|
||||
noCreateRemoteFilesButton.setMinHeight(16);
|
||||
noCreateRemoteFilesButton.setMaxHeight(16);
|
||||
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
|
||||
list.getContent().add(requireNoCreateRemoteFilesButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
|
||||
BorderPane skipCurseForgeRemoteFiles = new BorderPane();
|
||||
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")));
|
||||
list.getContent().add(skipCurseForgeRemoteFiles);
|
||||
|
||||
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
|
||||
var skipCurseForgeRemoteFilesButton = new LineToggleButton();
|
||||
skipCurseForgeRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"));
|
||||
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
|
||||
skipCurseForgeRemoteFilesButton.setSize(8);
|
||||
skipCurseForgeRemoteFilesButton.setMinHeight(16);
|
||||
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
|
||||
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
|
||||
|
||||
list.getContent().add(skipCurseForgeRemoteFilesButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,5 +379,31 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
}
|
||||
|
||||
private LinePane createTextFieldLinePane(String title, StringProperty property, ValidatorBase... validators) {
|
||||
LinePane linePane = new LinePane();
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.setMinWidth(500);
|
||||
|
||||
linePane.setTitle(title);
|
||||
linePane.setRight(textField);
|
||||
textField.textProperty().bindBidirectional(property);
|
||||
|
||||
boolean needValidation = false;
|
||||
if (validators != null) {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator != null) {
|
||||
needValidation = true;
|
||||
textField.getValidators().add(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needValidation) {
|
||||
FXUtils.setValidateWhileTextChanged(textField, true);
|
||||
validatingFields.add(textField);
|
||||
}
|
||||
|
||||
return linePane;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import com.google.gson.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -30,8 +26,10 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -42,20 +40,22 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AboutPage extends StackPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public AboutPage() {
|
||||
ComponentList about = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
|
||||
launcher.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
|
||||
launcher.setLargeTitle(true);
|
||||
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
launcher.setTitle("Hello Minecraft! Launcher");
|
||||
launcher.setSubtitle(Metadata.VERSION);
|
||||
launcher.setExternalLink(Metadata.PUBLISH_URL);
|
||||
|
||||
IconedTwoLineListItem author = new IconedTwoLineListItem();
|
||||
author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
|
||||
var author = LineButton.createExternalLinkButton("https://space.bilibili.com/1445341");
|
||||
author.setLargeTitle(true);
|
||||
author.setLeading(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
|
||||
author.setTitle("huanghongxun");
|
||||
author.setSubtitle(i18n("about.author.statement"));
|
||||
author.setExternalLink("https://space.bilibili.com/1445341");
|
||||
|
||||
about.getContent().setAll(launcher, author);
|
||||
}
|
||||
@@ -66,20 +66,20 @@ public final class AboutPage extends StackPane {
|
||||
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem copyright = new IconedTwoLineListItem();
|
||||
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
|
||||
copyright.setLargeTitle(true);
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
copyright.setExternalLink(Metadata.ABOUT_URL);
|
||||
|
||||
IconedTwoLineListItem claim = new IconedTwoLineListItem();
|
||||
var claim = LineButton.createExternalLinkButton(Metadata.EULA_URL);
|
||||
claim.setLargeTitle(true);
|
||||
claim.setTitle(i18n("about.claim"));
|
||||
claim.setSubtitle(i18n("about.claim.statement"));
|
||||
claim.setExternalLink(Metadata.EULA_URL);
|
||||
|
||||
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
|
||||
var openSource = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL");
|
||||
openSource.setLargeTitle(true);
|
||||
openSource.setTitle(i18n("about.open_source"));
|
||||
openSource.setSubtitle(i18n("about.open_source.statement"));
|
||||
openSource.setExternalLink("https://github.com/HMCL-dev/HMCL");
|
||||
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public final class AboutPage extends StackPane {
|
||||
: new Image(url);
|
||||
}
|
||||
|
||||
private static ComponentList loadIconedTwoLineList(String path) {
|
||||
private ComponentList loadIconedTwoLineList(String path) {
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
InputStream input = FXUtils.class.getResourceAsStream(path);
|
||||
@@ -127,36 +127,42 @@ public final class AboutPage extends StackPane {
|
||||
|
||||
for (JsonElement element : array) {
|
||||
JsonObject obj = element.getAsJsonObject();
|
||||
IconedTwoLineListItem item = new IconedTwoLineListItem();
|
||||
|
||||
var button = new LineButton();
|
||||
button.setLargeTitle(true);
|
||||
|
||||
if (obj.get("externalLink") instanceof JsonPrimitive externalLink) {
|
||||
button.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
|
||||
String link = externalLink.getAsString();
|
||||
button.setOnAction(event -> FXUtils.openLink(link));
|
||||
}
|
||||
|
||||
if (obj.has("image")) {
|
||||
JsonElement image = obj.get("image");
|
||||
if (image.isJsonPrimitive()) {
|
||||
item.setImage(loadImage(image.getAsString()));
|
||||
button.setLeading(loadImage(image.getAsString()));
|
||||
} else if (image.isJsonObject()) {
|
||||
item.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
|
||||
.then(loadImage(image.getAsJsonObject().get("dark").getAsString()))
|
||||
.otherwise(loadImage(image.getAsJsonObject().get("light").getAsString())));
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
button.setLeading(darkMode
|
||||
? loadImage(image.getAsJsonObject().get("dark").getAsString())
|
||||
: loadImage(image.getAsJsonObject().get("light").getAsString())
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.has("title"))
|
||||
item.setTitle(obj.get("title").getAsString());
|
||||
else if (obj.has("titleLocalized"))
|
||||
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
|
||||
if (obj.get("title") instanceof JsonPrimitive title)
|
||||
button.setTitle(title.getAsString());
|
||||
else if (obj.get("titleLocalized") instanceof JsonPrimitive titleLocalized)
|
||||
button.setTitle(i18n(titleLocalized.getAsString()));
|
||||
|
||||
if (obj.has("subtitle"))
|
||||
item.setSubtitle(obj.get("subtitle").getAsString());
|
||||
else if (obj.has("subtitleLocalized"))
|
||||
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
|
||||
if (obj.get("subtitle") instanceof JsonPrimitive subtitle)
|
||||
button.setSubtitle(subtitle.getAsString());
|
||||
else if (obj.get("subtitleLocalized") instanceof JsonPrimitive subtitleLocalized)
|
||||
button.setSubtitle(i18n(subtitleLocalized.getAsString()));
|
||||
|
||||
if (obj.has("externalLink")) {
|
||||
String link = obj.get("externalLink").getAsString();
|
||||
item.setExternalLink(link);
|
||||
FXUtils.installFastTooltip(item.getExternalLinkButton(), link);
|
||||
}
|
||||
|
||||
componentList.getContent().add(item);
|
||||
componentList.getContent().add(button);
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Failed to load list: " + path, e);
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public FeedbackPage() {
|
||||
VBox content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
@@ -45,30 +47,33 @@ public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
ComponentList groups = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
||||
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
var users = LineButton.createExternalLinkButton(Metadata.GROUPS_URL);
|
||||
users.setLargeTitle(true);
|
||||
users.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
users.setTitle(i18n("contact.chat.qq_group"));
|
||||
users.setSubtitle(i18n("contact.chat.qq_group.statement"));
|
||||
users.setExternalLink(Metadata.GROUPS_URL);
|
||||
|
||||
IconedTwoLineListItem discord = new IconedTwoLineListItem();
|
||||
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
|
||||
var discord = LineButton.createExternalLinkButton("https://discord.gg/jVvC7HfM6U");
|
||||
discord.setLargeTitle(true);
|
||||
discord.setLeading(FXUtils.newBuiltinImage("/assets/img/discord.png"));
|
||||
discord.setTitle(i18n("contact.chat.discord"));
|
||||
discord.setSubtitle(i18n("contact.chat.discord.statement"));
|
||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||
|
||||
groups.getContent().setAll(users, discord);
|
||||
}
|
||||
|
||||
ComponentList feedback = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem github = new IconedTwoLineListItem();
|
||||
github.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
|
||||
.then(FXUtils.newBuiltinImage("/assets/img/github-white.png"))
|
||||
.otherwise(FXUtils.newBuiltinImage("/assets/img/github.png")));
|
||||
var github = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL/issues/new/choose");
|
||||
github.setLargeTitle(true);
|
||||
github.setTitle(i18n("contact.feedback.github"));
|
||||
github.setSubtitle(i18n("contact.feedback.github.statement"));
|
||||
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
github.setLeading(darkMode
|
||||
? FXUtils.newBuiltinImage("/assets/img/github-white.png")
|
||||
: FXUtils.newBuiltinImage("/assets/img/github.png"));
|
||||
}));
|
||||
|
||||
feedback.getContent().setAll(github);
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
@@ -50,10 +50,11 @@ public class HelpPage extends SpinnerPane {
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setContent(scrollPane);
|
||||
|
||||
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
|
||||
var docPane = LineButton.createExternalLinkButton(Metadata.DOCS_URL);
|
||||
docPane.setLargeTitle(true);
|
||||
docPane.setTitle(i18n("help.doc"));
|
||||
docPane.setSubtitle(i18n("help.detail"));
|
||||
docPane.setExternalLink(Metadata.DOCS_URL);
|
||||
|
||||
ComponentList doc = new ComponentList();
|
||||
doc.getContent().setAll(docPane);
|
||||
content.getChildren().add(doc);
|
||||
@@ -68,77 +69,32 @@ public class HelpPage extends SpinnerPane {
|
||||
for (HelpCategory category : helpCategories) {
|
||||
ComponentList categoryPane = new ComponentList();
|
||||
|
||||
for (Help help : category.getItems()) {
|
||||
IconedTwoLineListItem item = new IconedTwoLineListItem();
|
||||
item.setTitle(help.getTitle());
|
||||
item.setSubtitle(help.getSubtitle());
|
||||
item.setExternalLink(help.getUrl());
|
||||
for (Help help : category.items()) {
|
||||
var item = LineButton.createExternalLinkButton(help.url());
|
||||
item.setLargeTitle(true);
|
||||
item.setTitle(help.title());
|
||||
item.setSubtitle(help.subtitle());
|
||||
|
||||
categoryPane.getContent().add(item);
|
||||
}
|
||||
|
||||
content.getChildren().add(ComponentList.createComponentListTitle(category.title));
|
||||
content.getChildren().add(ComponentList.createComponentListTitle(category.title()));
|
||||
content.getChildren().add(categoryPane);
|
||||
}
|
||||
hideSpinner();
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static class HelpCategory {
|
||||
@SerializedName("title")
|
||||
private final String title;
|
||||
|
||||
@SerializedName("items")
|
||||
private final List<Help> items;
|
||||
|
||||
public HelpCategory() {
|
||||
this("", Collections.emptyList());
|
||||
}
|
||||
|
||||
public HelpCategory(String title, List<Help> items) {
|
||||
this.title = title;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<Help> getItems() {
|
||||
return items;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record HelpCategory(
|
||||
@SerializedName("title") String title,
|
||||
@SerializedName("items") List<Help> items) {
|
||||
}
|
||||
|
||||
private static class Help {
|
||||
@SerializedName("title")
|
||||
private final String title;
|
||||
|
||||
@SerializedName("subtitle")
|
||||
private final String subtitle;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
public Help() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public Help(String title, String subtitle, String url) {
|
||||
this.title = title;
|
||||
this.subtitle = subtitle;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record Help(
|
||||
@SerializedName("title") String title,
|
||||
@SerializedName("subtitle") String subtitle,
|
||||
@SerializedName("url") String url) {
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -48,6 +48,8 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Result;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -108,23 +110,39 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
}
|
||||
|
||||
private final class DownloadMojangJava extends DialogPane {
|
||||
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||
private final List<JFXCheckBox> checkboxes = new ArrayList<>();
|
||||
|
||||
DownloadMojangJava() {
|
||||
setTitle(i18n("java.download"));
|
||||
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
|
||||
setTitle(i18n("java.download.title"));
|
||||
|
||||
VBox vbox = new VBox(16);
|
||||
Label prompt = new Label(i18n("java.download.prompt"));
|
||||
vbox.getChildren().add(prompt);
|
||||
|
||||
setValid(false);
|
||||
InvalidationListener updateValidStatus = e -> {
|
||||
for (JFXCheckBox box : checkboxes) {
|
||||
if (!box.isDisabled() && box.isSelected()) {
|
||||
setValid(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setValid(false);
|
||||
};
|
||||
|
||||
for (GameJavaVersion version : supportedGameJavaVersions) {
|
||||
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
|
||||
JFXCheckBox button = new JFXCheckBox("Java " + version.majorVersion());
|
||||
button.setUserData(version);
|
||||
vbox.getChildren().add(button);
|
||||
toggleGroup.getToggles().add(button);
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, version))
|
||||
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, version)) {
|
||||
button.setDisable(true);
|
||||
button.setSelected(true);
|
||||
} else {
|
||||
button.selectedProperty().addListener(updateValidStatus);
|
||||
}
|
||||
|
||||
vbox.getChildren().add(button);
|
||||
checkboxes.add(button);
|
||||
}
|
||||
|
||||
setBody(vbox);
|
||||
@@ -137,38 +155,52 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
setActions(warningLabel, acceptPane, cancelButton);
|
||||
}
|
||||
|
||||
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
|
||||
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
Throwable resolvedException = resolveException(exception);
|
||||
LOG.warning("Failed to download java", exception);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
|
||||
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
|
||||
if (javaVersion == null)
|
||||
List<GameJavaVersion> selectedVersions = new ArrayList<>();
|
||||
for (JFXCheckBox box : checkboxes) {
|
||||
if (!box.isDisabled() && box.isSelected()) {
|
||||
selectedVersions.add((GameJavaVersion) box.getUserData());
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedVersions.isEmpty())
|
||||
return;
|
||||
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
|
||||
Controllers.confirm(i18n("download.java.override"), null, () -> {
|
||||
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
|
||||
.thenComposeAsync(Schedulers.javafx(), realPath -> {
|
||||
if (realPath != null) {
|
||||
JavaManager.removeJava(realPath);
|
||||
Task<Void> task = Task.allOf(selectedVersions.stream().map(javaVersion -> JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).wrapResult()).toList())
|
||||
.whenComplete(Schedulers.javafx(), (results, exception) -> {
|
||||
Throwable exceptionToDisplay;
|
||||
if (exception == null) {
|
||||
List<Throwable> exceptions = results.stream()
|
||||
.filter(Result::isFailure)
|
||||
.map(Result::getException)
|
||||
.map(Lang::resolveException)
|
||||
.filter(it -> !(it instanceof CancellationException))
|
||||
.toList();
|
||||
if (!exceptions.isEmpty()) {
|
||||
if (exceptions.size() == 1) {
|
||||
exceptionToDisplay = exceptions.get(0);
|
||||
} else {
|
||||
exceptionToDisplay = new IOException("Failed to download Java");
|
||||
for (Throwable e : exceptions)
|
||||
exceptionToDisplay.addSuppressed(e);
|
||||
}
|
||||
return downloadTask(javaVersion);
|
||||
}), i18n("download.java"), TaskCancellationAction.NORMAL);
|
||||
}, null);
|
||||
else
|
||||
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
exceptionToDisplay = null;
|
||||
}
|
||||
} else {
|
||||
exceptionToDisplay = exception;
|
||||
}
|
||||
|
||||
if (exceptionToDisplay != null) {
|
||||
LOG.warning("Failed to download java", exceptionToDisplay);
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
|
||||
}
|
||||
});
|
||||
|
||||
Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +343,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
FXUtils.onChange(distributionBox.getSelectionModel().selectedItemProperty(),
|
||||
it -> currentJavaVersionList.set(getJavaVersionList(it)));
|
||||
|
||||
setHeading(new Label(i18n("java.download")));
|
||||
setHeading(new Label(i18n("java.download.title")));
|
||||
setBody(body);
|
||||
setActions(warningLabel, downloadButtonPane, cancelButton);
|
||||
if (platform.getOperatingSystem() == OperatingSystem.LINUX && platform.getArchitecture() == Architecture.RISCV64) {
|
||||
|
||||
@@ -115,7 +115,6 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
nameField = new JFXTextField();
|
||||
nameField.textProperty().bindBidirectional(control.nameProperty);
|
||||
FXUtils.setLimitWidth(nameField, 200);
|
||||
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
|
||||
namePane.setRight(nameField);
|
||||
nameField.setValidators(
|
||||
new RequiredValidator(),
|
||||
|
||||
@@ -30,6 +30,7 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.java.JavaInfo;
|
||||
@@ -189,7 +190,6 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
JavaPageSkin(JavaManagementPage skinnable) {
|
||||
super(skinnable);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,6 +232,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
center.getChildren().setAll(content);
|
||||
@@ -241,7 +242,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setOnAction(e -> {
|
||||
JavaRuntime java = getItem();
|
||||
|
||||
@@ -159,7 +159,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
|
||||
revealButton.setOnAction(e -> skinnable.onReveal());
|
||||
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE.createIcon(24), 24, 24));
|
||||
removeButton.setGraphic(SVG.DELETE.createIcon());
|
||||
removeButton.setOnAction(e -> skinnable.onRemove());
|
||||
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
|
||||
|
||||
@@ -176,7 +176,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
} else {
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
|
||||
restoreButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
restoreButton.setOnAction(e -> skinnable.onRestore());
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
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.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -320,6 +320,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
.thenSupplyAsync(() -> versionList.getVersions("").stream()
|
||||
.filter(it -> it.getVersionType() == RELEASE)
|
||||
.filter(it -> NativePatcher.checkSupportedStatus(GameVersionNumber.asGameVersion(it.getGameVersion()), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
@@ -65,7 +66,6 @@ import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -171,16 +171,14 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED));
|
||||
gameItem.setActionButtonVisible(false);
|
||||
gameItem.setLeftIcon(SVG.FORMAT_LIST_BULLETED);
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
|
||||
|
||||
// forth item in left sidebar
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD));
|
||||
downloadItem.setActionButtonVisible(false);
|
||||
downloadItem.setLeftIcon(SVG.DOWNLOAD);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> {
|
||||
Controllers.getDownloadPage().showGameDownloads();
|
||||
@@ -193,8 +191,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// fifth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
|
||||
launcherSettingsItem.setActionButtonVisible(false);
|
||||
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
launcherSettingsItem.setOnAction(e -> {
|
||||
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
|
||||
@@ -206,8 +203,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem terracottaItem = new AdvancedListItem();
|
||||
terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2));
|
||||
terracottaItem.setActionButtonVisible(false);
|
||||
terracottaItem.setLeftIcon(SVG.GRAPH2);
|
||||
terracottaItem.setTitle(i18n("terracotta"));
|
||||
terracottaItem.setOnAction(e -> {
|
||||
if (TerracottaMetadata.PROVIDER != null) {
|
||||
@@ -249,7 +245,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
setCenter(getSkinnable().getMainPage());
|
||||
}
|
||||
|
||||
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
|
||||
public void showGameListPopupMenu(Region gameListItem) {
|
||||
GameListPopupMenu.show(gameListItem,
|
||||
JFXPopup.PopupVPosition.TOP,
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
|
||||
@@ -178,20 +178,21 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
VBox content = new VBox();
|
||||
content.setSpacing(8);
|
||||
|
||||
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
|
||||
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
|
||||
VBox content = new VBox(12);
|
||||
content.setPadding(new Insets(8, 0, 0, 0));
|
||||
|
||||
updateChannelGroup = new ToggleGroup();
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
|
||||
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(10, 0, 0, 0));
|
||||
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
|
||||
|
||||
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public final class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
if (tree != null) tree.setExpanded(true);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
@@ -141,6 +142,9 @@ public final class NBTTreeView extends TreeView<Tag> {
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
|
||||
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.profile;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
@@ -35,7 +36,6 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
public ProfileListItemSkin(ProfileListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPickOnBounds(false);
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
@@ -46,7 +46,9 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
|
||||
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
|
||||
|
||||
Node left = FXUtils.wrap(SVG.FOLDER);
|
||||
Node left = SVG.FOLDER.createIcon(20);
|
||||
left.setMouseTransparent(true);
|
||||
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
|
||||
root.setLeft(left);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -35,10 +35,7 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -54,7 +51,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
private final StringProperty location;
|
||||
private final Profile profile;
|
||||
private final JFXTextField txtProfileName;
|
||||
private final FileItem gameDir;
|
||||
private final LineFileChooserButton gameDir;
|
||||
private final LineToggleButton toggleUseRelativePath;
|
||||
|
||||
/**
|
||||
@@ -107,10 +104,11 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
});
|
||||
}
|
||||
|
||||
gameDir = new FileItem();
|
||||
gameDir.setName(i18n("profile.instance_directory"));
|
||||
gameDir.setTitle(i18n("profile.instance_directory.choose"));
|
||||
gameDir.pathProperty().bindBidirectional(location);
|
||||
gameDir = new LineFileChooserButton();
|
||||
gameDir.setTitle(i18n("profile.instance_directory"));
|
||||
gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose"));
|
||||
gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY);
|
||||
gameDir.locationProperty().bindBidirectional(location);
|
||||
|
||||
toggleUseRelativePath = new LineToggleButton();
|
||||
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
|
||||
@@ -188,7 +186,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
}
|
||||
} else {
|
||||
if (StringUtils.isBlank(getLocation())) {
|
||||
gameDir.onExplore();
|
||||
gameDir.fire();
|
||||
}
|
||||
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
|
||||
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
|
||||
|
||||
+65
-175
@@ -22,8 +22,6 @@ import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -34,13 +32,8 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -57,13 +50,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
@@ -163,12 +150,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
body.getStyleClass().add("terracotta-hint");
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton download = LineButton.of();
|
||||
download.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
var download = createLargeTitleLineButton();
|
||||
download.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork)));
|
||||
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
|
||||
download.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(download, () -> {
|
||||
download.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
download.setOnAction(event -> {
|
||||
TerracottaState.Preparing s = TerracottaManager.download();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -207,12 +194,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
flow.getStyleClass().add("terracotta-hint");
|
||||
flow.setLineSpacing(4);
|
||||
|
||||
LineButton host = LineButton.of();
|
||||
host.setLeftIcon(SVG.HOST);
|
||||
var host = createLargeTitleLineButton();
|
||||
host.setLeading(SVG.HOST, ICON_SIZE);
|
||||
host.setTitle(i18n("terracotta.status.waiting.host.title"));
|
||||
host.setSubtitle(i18n("terracotta.status.waiting.host.desc"));
|
||||
host.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(host, () -> {
|
||||
host.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
host.setOnAction(event -> {
|
||||
if (LauncherHelper.countMangedProcesses() >= 1) {
|
||||
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
|
||||
if (s1 != null) {
|
||||
@@ -239,12 +226,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
});
|
||||
|
||||
LineButton guest = LineButton.of();
|
||||
guest.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
var guest = createLargeTitleLineButton();
|
||||
guest.setLeading(SVG.ADD_CIRCLE, ICON_SIZE);
|
||||
guest.setTitle(i18n("terracotta.status.waiting.guest.title"));
|
||||
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(guest, () -> {
|
||||
guest.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
guest.setOnAction(event -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
|
||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
@@ -263,11 +250,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
LineButton feedback = LineButton.of();
|
||||
feedback.setLeftIcon(SVG.FEEDBACK);
|
||||
var feedback = createLargeTitleLineButton();
|
||||
feedback.setLeading(SVG.FEEDBACK, ICON_SIZE);
|
||||
feedback.setTitle(i18n("terracotta.feedback.title"));
|
||||
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
|
||||
feedback.setRightIcon(SVG.OPEN_IN_NEW);
|
||||
feedback.setTrailingIcon(SVG.OPEN_IN_NEW, ICON_SIZE);
|
||||
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
|
||||
nodesProperty.setAll(flow, host, guest, feedback);
|
||||
@@ -282,11 +269,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
body.getStyleClass().add("terracotta-hint");
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.scanning.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -298,11 +285,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.host_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.host_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -342,17 +329,17 @@ public class TerracottaControllerPage extends StackPane {
|
||||
code.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(code, () -> copyCode(cs));
|
||||
|
||||
LineButton copy = LineButton.of();
|
||||
copy.setLeftIcon(SVG.CONTENT_COPY);
|
||||
var copy = createLargeTitleLineButton();
|
||||
copy.setLeading(SVG.CONTENT_COPY, ICON_SIZE);
|
||||
copy.setTitle(i18n("terracotta.status.host_ok.code.copy"));
|
||||
copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc"));
|
||||
FXUtils.onClicked(copy, () -> copyCode(cs));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.host_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -369,11 +356,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.guest_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.guest_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -384,12 +371,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
if (state instanceof TerracottaState.GuestStarting) {
|
||||
TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty();
|
||||
if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) {
|
||||
LineButton info = LineButton.of();
|
||||
info.setLeftIcon(switch (difficulty) {
|
||||
var info = createLargeTitleLineButton();
|
||||
info.setLeading(switch (difficulty) {
|
||||
case UNKNOWN -> throw new AssertionError();
|
||||
case EASIEST, SIMPLE -> SVG.INFO;
|
||||
case MEDIUM, TOUGH -> SVG.WARNING;
|
||||
});
|
||||
}, ICON_SIZE);
|
||||
|
||||
String difficultyID = difficulty.name().toLowerCase(Locale.ROOT);
|
||||
info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID)));
|
||||
@@ -412,15 +399,15 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.guest_ok"));
|
||||
progressProperty.set(1);
|
||||
|
||||
LineButton tutorial = LineButton.of();
|
||||
var tutorial = createLargeTitleLineButton();
|
||||
tutorial.setTitle(i18n("terracotta.status.guest_ok.title"));
|
||||
tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl()));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.guest_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -438,11 +425,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
progressProperty.set(1);
|
||||
nodesProperty.setAll();
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.exception.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -450,8 +437,8 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
|
||||
SpinnerPane exportLog = new SpinnerPane();
|
||||
LineButton exportLogInner = LineButton.of();
|
||||
exportLogInner.setLeftIcon(SVG.OUTPUT);
|
||||
var exportLogInner = createLargeTitleLineButton();
|
||||
exportLogInner.setLeading(SVG.OUTPUT, ICON_SIZE);
|
||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||
exportLog.setContent(exportLogInner);
|
||||
@@ -459,7 +446,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
// FIXME: SpinnerPane loses its content width in loading state.
|
||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||
|
||||
FXUtils.onClicked(exportLogInner, () -> {
|
||||
exportLogInner.setOnAction(event -> {
|
||||
exportLog.setLoading(true);
|
||||
|
||||
TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> {
|
||||
@@ -493,11 +480,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
progressProperty.set(1);
|
||||
|
||||
if (fatal.isRecoverable()) {
|
||||
LineButton retry = LineButton.of();
|
||||
retry.setLeftIcon(SVG.RESTORE);
|
||||
var retry = createLargeTitleLineButton();
|
||||
retry.setLeading(SVG.RESTORE, ICON_SIZE);
|
||||
retry.setTitle(i18n("terracotta.status.fatal.retry"));
|
||||
retry.setSubtitle(message);
|
||||
FXUtils.onClicked(retry, () -> {
|
||||
retry.setOnAction(event -> {
|
||||
TerracottaState s = TerracottaManager.recover();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -534,7 +521,8 @@ public class TerracottaControllerPage extends StackPane {
|
||||
children.add(statusPane);
|
||||
children.addAll(nodesProperty);
|
||||
}
|
||||
|
||||
// Prevent the shadow of components from being clipped
|
||||
StackPane.setMargin(components, new Insets(0, 0, 5, 0));
|
||||
transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN);
|
||||
};
|
||||
listener.changed(UI_STATE, null, UI_STATE.get());
|
||||
@@ -561,34 +549,27 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
locals.setComponentPadding(false);
|
||||
|
||||
LineButton header = LineButton.of(false);
|
||||
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
var header = new LinePane();
|
||||
header.getStyleClass().add("no-padding");
|
||||
header.setLargeTitle(true);
|
||||
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
|
||||
header.setMouseTransparent(true);
|
||||
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
header.setTitle(i18n("terracotta.from_local.title"));
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
HBox node = new HBox();
|
||||
node.setAlignment(Pos.CENTER_LEFT);
|
||||
node.setPadding(new Insets(10, 16, 10, 16));
|
||||
|
||||
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
Node icon = SVG.OPEN_IN_NEW.createIcon(16);
|
||||
node.getChildren().setAll(description, placeholder, icon);
|
||||
|
||||
String url = link.link();
|
||||
RipplerContainer container = new RipplerContainer(node);
|
||||
container.setOnMouseClicked(ev -> Controllers.dialog(
|
||||
LineButton item = new LineButton();
|
||||
item.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
item.setTitle(link.description().getText(I18n.getLocale().getCandidateLocales()));
|
||||
item.setOnAction(event -> Controllers.dialog(
|
||||
i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME),
|
||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||
() -> FXUtils.openLink(url)
|
||||
() -> FXUtils.openLink(link.link())
|
||||
));
|
||||
ComponentList.setNoPadding(container);
|
||||
locals.getContent().add(container);
|
||||
locals.getContent().add(item);
|
||||
}
|
||||
return locals;
|
||||
}
|
||||
@@ -597,103 +578,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast"));
|
||||
}
|
||||
|
||||
private static final class LineButton extends RipplerContainer {
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
private static final double ICON_SIZE = 28;
|
||||
|
||||
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
|
||||
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "");
|
||||
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
|
||||
|
||||
public static LineButton of() {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
public static LineButton of(boolean padding) {
|
||||
HBox container = new HBox();
|
||||
if (padding) {
|
||||
container.setPadding(new Insets(10, 16, 10, 16));
|
||||
}
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
container.setCursor(Cursor.HAND);
|
||||
container.setSpacing(16);
|
||||
|
||||
LineButton button = new LineButton(container);
|
||||
VBox spacing = new VBox();
|
||||
HBox.setHgrow(spacing, Priority.ALWAYS);
|
||||
button.holder.add(FXUtils.observeWeak(() -> {
|
||||
List<Node> nodes = new ArrayList<>(4);
|
||||
Node left = button.left.get();
|
||||
if (left != null) {
|
||||
nodes.add(left);
|
||||
}
|
||||
|
||||
{
|
||||
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
|
||||
VBox middle = new VBox();
|
||||
middle.getStyleClass().add("two-line-list-item");
|
||||
middle.setMouseTransparent(true);
|
||||
{
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
{
|
||||
Label lblTitle = new Label(button.title.get());
|
||||
lblTitle.getStyleClass().add("title");
|
||||
firstLine.getChildren().setAll(lblTitle);
|
||||
}
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
secondLine.getStyleClass().add("second-line");
|
||||
{
|
||||
Text text = new Text(button.subTitle.get());
|
||||
|
||||
TextFlow lblSubtitle = new TextFlow(text);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
}
|
||||
|
||||
middle.getChildren().setAll(firstLine, secondLine);
|
||||
}
|
||||
nodes.add(middle);
|
||||
}
|
||||
|
||||
nodes.add(spacing);
|
||||
|
||||
Node right = button.right.get();
|
||||
if (right != null) {
|
||||
nodes.add(right);
|
||||
}
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
ComponentList.setNoPadding(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private LineButton(Node container) {
|
||||
super(container);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subTitle.set(subtitle);
|
||||
}
|
||||
|
||||
public void setLeftImage(Image left) {
|
||||
this.left.set(new ImageView(left));
|
||||
}
|
||||
|
||||
public void setLeftIcon(SVG left) {
|
||||
this.left.set(left.createIcon(28));
|
||||
}
|
||||
|
||||
public void setRightIcon(SVG right) {
|
||||
this.right.set(right.createIcon(28));
|
||||
}
|
||||
private static LineButton createLargeTitleLineButton() {
|
||||
var lineButton = new LineButton();
|
||||
lineButton.setLargeTitle(true);
|
||||
return lineButton;
|
||||
}
|
||||
|
||||
private static final class PlayerProfileUI extends VBox {
|
||||
|
||||
@@ -88,8 +88,8 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("datapack.choose_datapack"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
|
||||
chooser.setTitle(i18n("datapack.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null) {
|
||||
|
||||
@@ -183,7 +183,6 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
@@ -244,6 +245,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
@@ -521,7 +524,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
||||
listView.setSelectionModel(null);
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
|
||||
@@ -27,9 +27,18 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
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.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
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.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -46,7 +55,11 @@ 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.*;
|
||||
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.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
@@ -54,7 +67,14 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
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.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -518,8 +538,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) {
|
||||
spinnerPane.setLoading(true);
|
||||
Task.supplyAsync(() -> {
|
||||
Task.composeAsync(() -> {
|
||||
// TODO: Massive tasks may cause OOM.
|
||||
EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
|
||||
List<Task<?>> queue = new ArrayList<>(version.getDependencies().size());
|
||||
for (RemoteMod.Dependency dependency : version.getDependencies()) {
|
||||
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
|
||||
continue;
|
||||
@@ -532,11 +554,22 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
list.add(title);
|
||||
dependencies.put(dependency.getType(), list);
|
||||
}
|
||||
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback);
|
||||
dependencies.get(dependency.getType()).add(dependencyModItem);
|
||||
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenAcceptAsync(Schedulers.javafx(), dep -> {
|
||||
if (dep == RemoteMod.BROKEN) {
|
||||
return;
|
||||
}
|
||||
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dep, selfPage.version, selfPage.callback);
|
||||
dependencies.get(dependency.getType()).add(dependencyModItem);
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR));
|
||||
}
|
||||
|
||||
return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
|
||||
return Task.allOf(queue).thenSupplyAsync(() ->
|
||||
dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())
|
||||
);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
spinnerPane.setLoading(false);
|
||||
if (exception == null) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
@@ -26,7 +27,6 @@ import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -39,14 +39,20 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
@SuppressWarnings("unused")
|
||||
private Consumer<Event> onVersionIconChangedListener;
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
public GameAdvancedListItem() {
|
||||
Pair<Node, ImageView> view = createImageView(null);
|
||||
setLeftGraphic(view.getKey());
|
||||
imageView = view.getValue();
|
||||
this.imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(null);
|
||||
|
||||
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
|
||||
imageView.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
|
||||
setLeftGraphic(imageViewWrapper);
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
|
||||
|
||||
setActionButtonVisible(false);
|
||||
}
|
||||
|
||||
private void loadVersion(String version) {
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
item.update();
|
||||
});
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24));
|
||||
btnUpgrade.setGraphic(SVG.UPDATE.createIcon());
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
|
||||
@@ -127,7 +127,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
});
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
|
||||
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
|
||||
right.getChildren().add(btnLaunch);
|
||||
|
||||
@@ -143,7 +143,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24));
|
||||
btnManage.setGraphic(SVG.MORE_VERT.createIcon());
|
||||
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
|
||||
right.getChildren().add(btnManage);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
AdvancedListItem addProfileItem = new AdvancedListItem();
|
||||
addProfileItem.getStyleClass().add("navigation-drawer-item");
|
||||
addProfileItem.setTitle(i18n("profile.new"));
|
||||
addProfileItem.setActionButtonVisible(false);
|
||||
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
|
||||
addProfileItem.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
|
||||
|
||||
pane.setFitToWidth(true);
|
||||
|
||||
@@ -136,7 +136,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
|
||||
public void installOffline() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.choose_mod"));
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
@@ -199,7 +200,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
@@ -581,15 +581,15 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
|
||||
restoreButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER.createIcon());
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24));
|
||||
infoButton.setGraphic(SVG.INFO.createIcon());
|
||||
|
||||
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
|
||||
@@ -2,30 +2,27 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
|
||||
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.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -38,7 +35,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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;
|
||||
|
||||
@@ -115,8 +111,8 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
|
||||
public void onAddFiles() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("resourcepack.add"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
|
||||
fileChooser.setTitle(i18n("resourcepack.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.resourcepack"), "*.zip"));
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -128,45 +124,24 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
private static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
|
||||
private final JFXListView<ResourcepackInfoObject> listView;
|
||||
private static final class ResourcepackListPageSkin extends ToolbarListPageSkin<ResourcepackInfoObject, ResourcepackListPage> {
|
||||
|
||||
private ResourcepackListPageSkin(ResourcepackListPage control) {
|
||||
public ResourcepackListPageSkin(ResourcepackListPage control) {
|
||||
super(control);
|
||||
}
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh),
|
||||
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles),
|
||||
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload)
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(ResourcepackListPage skinnable) {
|
||||
return List.of(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, skinnable::onAddFiles),
|
||||
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, skinnable::onDownload)
|
||||
);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(control.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ResourcepackListCell(listView, control));
|
||||
Bindings.bindContent(listView.getItems(), control.getItems());
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
@Override
|
||||
protected ListCell<ResourcepackInfoObject> createListCell(JFXListView<ResourcepackInfoObject> listView) {
|
||||
return new ResourcepackListCell(listView, getSkinnable());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -146,9 +144,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
|
||||
public void onAddFiles() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("schematics.add"));
|
||||
fileChooser.setTitle(i18n("schematics.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
i18n("schematics"), "*.litematic"));
|
||||
i18n("extension.schematic"), "*.litematic"));
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
private final HBox right;
|
||||
|
||||
private final ImageView iconImageView;
|
||||
private final SVGPath iconSVG;
|
||||
private final StackPane iconSVGWrapper;
|
||||
private final SVGContainer iconSVGView;
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
this.iconImageView = new ImageView();
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
|
||||
this.iconSVG = new SVGPath();
|
||||
iconSVG.getStyleClass().add("svg");
|
||||
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
|
||||
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
|
||||
|
||||
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
|
||||
iconSVGWrapper.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(iconSVGWrapper, 32);
|
||||
FXUtils.setLimitHeight(iconSVGWrapper, 32);
|
||||
this.iconSVGView = new SVGContainer(32);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER);
|
||||
root.setLeft(left);
|
||||
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVG.setContent(item.getIcon().getPath());
|
||||
left.getChildren().setAll(iconSVGWrapper);
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
|
||||
@@ -175,27 +175,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
componentList = new ComponentList();
|
||||
|
||||
if (!globalSetting) {
|
||||
BorderPane copyGlobalPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.game.copy_global"));
|
||||
copyGlobalPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
var copyGlobalButton = LineButton.createNavigationButton();
|
||||
copyGlobalButton.setTitle(i18n("settings.game.copy_global"));
|
||||
copyGlobalButton.setOnAction(event ->
|
||||
Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
|
||||
Set<String> ignored = new HashSet<>(Arrays.asList(
|
||||
"usesGlobal",
|
||||
"versionIcon"
|
||||
));
|
||||
|
||||
JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all"));
|
||||
copyAll.disableProperty().bind(modpack);
|
||||
copyGlobalPane.setRight(copyAll);
|
||||
copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
|
||||
Set<String> ignored = new HashSet<>(Arrays.asList(
|
||||
"usesGlobal",
|
||||
"versionIcon"
|
||||
));
|
||||
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
|
||||
}, null));
|
||||
|
||||
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
|
||||
}, null));
|
||||
BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT);
|
||||
}
|
||||
|
||||
componentList.getContent().add(copyGlobalPane);
|
||||
componentList.getContent().add(copyGlobalButton);
|
||||
}
|
||||
|
||||
javaItem = new MultiFileItem<>();
|
||||
@@ -424,7 +416,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
|
||||
LineNavigationButton showAdvancedSettingPane = new LineNavigationButton();
|
||||
var showAdvancedSettingPane = LineButton.createNavigationButton();
|
||||
showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
|
||||
showAdvancedSettingPane.setOnAction(event -> {
|
||||
if (lastVersionSetting != null) {
|
||||
|
||||
@@ -103,7 +103,7 @@ public final class Versions {
|
||||
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
|
||||
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).executor(true),
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NORMAL
|
||||
);
|
||||
|
||||
@@ -20,17 +20,16 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
|
||||
import org.jackhuang.hmcl.ui.construct.LinePane;
|
||||
import org.jackhuang.hmcl.ui.construct.LineTextPane;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
@@ -42,47 +41,40 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
|
||||
public WorldExportPageSkin(WorldExportPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
Insets insets = new Insets(0, 0, 12, 0);
|
||||
VBox container = new VBox();
|
||||
container.setSpacing(16);
|
||||
container.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(container, 500);
|
||||
{
|
||||
HBox labelContainer = new HBox();
|
||||
labelContainer.setPadding(new Insets(0, 0, 0, 5));
|
||||
Label label = new Label(i18n("world.export"));
|
||||
labelContainer.getChildren().setAll(label);
|
||||
container.getChildren().add(labelContainer);
|
||||
}
|
||||
|
||||
ComponentList list = new ComponentList();
|
||||
|
||||
FileItem fileItem = new FileItem();
|
||||
fileItem.setName(i18n("world.export.location"));
|
||||
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty());
|
||||
list.getContent().add(fileItem);
|
||||
var chooseFileButton = new LineFileChooserButton();
|
||||
chooseFileButton.setTitle(i18n("world.export.location"));
|
||||
chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE);
|
||||
chooseFileButton.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
chooseFileButton.locationProperty().bindBidirectional(skinnable.pathProperty());
|
||||
|
||||
var worldNamePane = new LinePane();
|
||||
worldNamePane.setTitle(i18n("world.name"));
|
||||
JFXTextField txtWorldName = new JFXTextField();
|
||||
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
|
||||
txtWorldName.setLabelFloat(true);
|
||||
txtWorldName.setPromptText(i18n("world.name"));
|
||||
StackPane.setMargin(txtWorldName, insets);
|
||||
list.getContent().add(txtWorldName);
|
||||
txtWorldName.setPrefWidth(300);
|
||||
worldNamePane.setRight(txtWorldName);
|
||||
|
||||
Label lblGameVersionTitle = new Label(i18n("world.game_version"));
|
||||
Label lblGameVersion = new Label();
|
||||
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty());
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
|
||||
gameVersionPane.setLeft(lblGameVersionTitle);
|
||||
gameVersionPane.setRight(lblGameVersion);
|
||||
list.getContent().add(gameVersionPane);
|
||||
LineTextPane gameVersionPane = new LineTextPane();
|
||||
gameVersionPane.setTitle(i18n("world.game_version"));
|
||||
gameVersionPane.textProperty().bind(skinnable.gameVersionProperty());
|
||||
|
||||
container.getChildren().add(list);
|
||||
list.getContent().setAll(chooseFileButton, worldNamePane, gameVersionPane);
|
||||
|
||||
container.getChildren().setAll(
|
||||
ComponentList.createComponentListTitle(i18n("world.export")),
|
||||
list
|
||||
);
|
||||
|
||||
JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export"));
|
||||
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())),
|
||||
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty()));
|
||||
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())),
|
||||
txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty()));
|
||||
btnExport.setOnAction(e -> skinnable.export());
|
||||
HBox bottom = new HBox();
|
||||
bottom.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -31,7 +31,6 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.effect.BoxBlur;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -202,7 +201,6 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
HBox right = new HBox(8);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.getChildren().setAll(visibilityButton, seedLabel);
|
||||
seedPane.setRight(right);
|
||||
}
|
||||
@@ -410,9 +408,9 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) {
|
||||
private void setRightTextField(LinePane linePane, int perfWidth, Tag tag) {
|
||||
JFXTextField textField = new JFXTextField();
|
||||
setRightTextField(borderPane, textField, perfWidth);
|
||||
setRightTextField(linePane, textField, perfWidth);
|
||||
if (tag instanceof IntTag intTag) {
|
||||
bindTagAndTextField(intTag, textField);
|
||||
} else if (tag instanceof FloatTag floatTag) {
|
||||
@@ -422,11 +420,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
||||
private void setRightTextField(LinePane linePane, JFXTextField textField, int perfWidth) {
|
||||
textField.setDisable(isReadOnly);
|
||||
textField.setPrefWidth(perfWidth);
|
||||
BorderPane.setAlignment(textField, Pos.CENTER_RIGHT);
|
||||
borderPane.setRight(textField);
|
||||
linePane.setRight(textField);
|
||||
}
|
||||
|
||||
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -53,6 +54,7 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
@@ -67,6 +69,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
private List<World> worlds;
|
||||
private Profile profile;
|
||||
private String instanceId;
|
||||
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
|
||||
|
||||
private int refreshCount = 0;
|
||||
|
||||
@@ -121,6 +124,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
supportQuickPlay.set(World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion)));
|
||||
|
||||
worlds = result;
|
||||
updateWorldList();
|
||||
|
||||
@@ -133,8 +139,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("world.import.choose"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
|
||||
chooser.setTitle(i18n("world.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res == null || res.isEmpty()) return;
|
||||
@@ -158,16 +164,16 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
refresh();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
handler.reject(i18n("world.add.failed", i18n("world.add.already_exists")));
|
||||
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
||||
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
handler.reject(i18n("world.add.failed", i18n("install.new_game.malformed")));
|
||||
else
|
||||
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
handler.reject(i18n("world.add.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, e -> {
|
||||
LOG.warning("Unable to parse world file " + zipFile, e);
|
||||
Controllers.dialog(i18n("world.import.invalid"));
|
||||
Controllers.dialog(i18n("world.add.invalid"));
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -203,6 +209,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
return showAll;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty supportQuickPlayProperty() {
|
||||
return supportQuickPlay;
|
||||
}
|
||||
|
||||
private final class WorldListPageSkin extends ToolbarListPageSkin<World, WorldListPage> {
|
||||
|
||||
WorldListPageSkin() {
|
||||
@@ -269,6 +279,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnLaunch = new JFXButton();
|
||||
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
|
||||
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
|
||||
right.getChildren().add(btnLaunch);
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
|
||||
@@ -286,7 +298,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
btnMore.setOnAction(event -> {
|
||||
World world = getItem();
|
||||
if (world != null)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -302,7 +314,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
page.showManagePage(world);
|
||||
else if (event.getButton() == MouseButton.SECONDARY)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,19 +352,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
// Popup Menu
|
||||
|
||||
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
public void showPopupMenu(World world, boolean supportQuickPlay, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
boolean worldLocked = world.isLocked();
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
|
||||
if (world.supportQuickPlay()) {
|
||||
if (supportQuickPlay) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
launchItem.setDisable(worldLocked);
|
||||
popupMenu.getContent().add(launchItem);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
launchItem,
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
|
||||
@@ -35,11 +35,13 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -52,7 +54,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final Profile profile;
|
||||
private final String versionId;
|
||||
private final String instanceId;
|
||||
private final boolean supportQuickPlay;
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
private final ObjectProperty<State> state;
|
||||
@@ -66,11 +69,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
||||
|
||||
public WorldManagePage(World world, Profile profile, String versionId) {
|
||||
public WorldManagePage(World world, Profile profile, String instanceId) {
|
||||
this.world = world;
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(versionId);
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(instanceId);
|
||||
this.profile = profile;
|
||||
this.versionId = versionId;
|
||||
this.instanceId = instanceId;
|
||||
|
||||
updateSessionLockChannel();
|
||||
|
||||
@@ -87,6 +90,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
supportQuickPlay = World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion));
|
||||
|
||||
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
@@ -144,11 +150,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, versionId, world.getFileName());
|
||||
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName());
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -225,7 +231,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
{
|
||||
if (getSkinnable().world.supportQuickPlay()) {
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||
}
|
||||
|
||||
@@ -257,7 +263,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (getSkinnable().world.supportQuickPlay()) {
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class WorldManageUIUtils {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("world.export.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
fileChooser.setInitialFileName(world.getWorldName());
|
||||
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 io.nayuki.qrcodegen.QrCode;
|
||||
|
||||
/// @author Glavo
|
||||
public final class QrCodeUtils {
|
||||
public static String toSVGPath(QrCode qr) {
|
||||
return toSVGPath(qr, 0);
|
||||
}
|
||||
|
||||
public static String toSVGPath(QrCode qr, int border) {
|
||||
int actualSize = qr.size + border * 2;
|
||||
|
||||
var builder = new StringBuilder(qr.size * qr.size * 12);
|
||||
builder.append('M').append(actualSize).append(' ').append(actualSize).append("ZM0 0Z");
|
||||
|
||||
for (int y = 0; y < qr.size; y++) {
|
||||
for (int x = 0; x < qr.size; x++) {
|
||||
if (qr.getModule(x, y)) {
|
||||
if (x != 0 || y != 0)
|
||||
builder.append(' ');
|
||||
builder.append("M").append(x + border).append(',').append(y + border).append("h1v1h-1z");
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private QrCodeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
{
|
||||
"title": "EasyTier",
|
||||
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy",
|
||||
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
|
||||
"externalLink": "https://github.com/EasyTier/EasyTier"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -324,20 +324,25 @@
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line > .tag {
|
||||
.two-line-list-item > .first-line .scroll-bar {
|
||||
-fx-opacity: 0;
|
||||
-fx-max-height: 0;
|
||||
-fx-min-height: 0;
|
||||
-fx-pref-height: 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line .tag {
|
||||
-fx-text-fill: -monet-on-secondary-container;
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
-fx-background-radius: 2;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line > .tag-warning {
|
||||
.two-line-list-item > .first-line .tag:warning {
|
||||
-fx-text-fill: -monet-on-error-container;
|
||||
-fx-background-color: -fixed-warning-tag-background;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-item-second-large {
|
||||
@@ -567,6 +572,41 @@
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Microsoft Login Dialog *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.microsoft-login-dialog:bodyonly {
|
||||
-fx-padding: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.method-title {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
-fx-text-fill: -monet-outline;
|
||||
-fx-font-size: 0.9em;
|
||||
-fx-line-spacing: 2;
|
||||
-fx-wrap-text: true;
|
||||
-fx-text-alignment: center;
|
||||
}
|
||||
|
||||
.code-box {
|
||||
-fx-background-color: -monet-surface-variant;
|
||||
-fx-background-radius: 6;
|
||||
-fx-padding: 0 10 0 10;
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.code-box .code-label {
|
||||
-fx-font-size: 22px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
@@ -587,8 +627,16 @@
|
||||
-fx-pref-width: 150.0px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Snack Bar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.jfx-snackbar-content {
|
||||
-fx-background-color: -monet-inverse-surface;
|
||||
-fx-padding: 5;
|
||||
-fx-spacing: 5;
|
||||
}
|
||||
|
||||
.jfx-snackbar-toast {
|
||||
@@ -1076,6 +1124,14 @@
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Toggle Button *
|
||||
@@ -1103,15 +1159,16 @@
|
||||
.jfx-toggle-button Line {
|
||||
-fx-stroke: -jfx-untoggle-line-color;
|
||||
}
|
||||
.jfx-toggle-button:selected Line{
|
||||
|
||||
.jfx-toggle-button:selected Line {
|
||||
-fx-stroke: -jfx-toggle-line-color;
|
||||
}
|
||||
|
||||
.jfx-toggle-button Circle{
|
||||
.jfx-toggle-button Circle {
|
||||
-fx-fill: -jfx-untoggle-color;
|
||||
}
|
||||
|
||||
.jfx-toggle-button:selected Circle{
|
||||
.jfx-toggle-button:selected Circle {
|
||||
-fx-fill: -jfx-toggle-color;
|
||||
}
|
||||
|
||||
@@ -1268,6 +1325,7 @@
|
||||
|
||||
.log-toggle {
|
||||
-fx-border: 1px;
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-color: -fixed-log-toggle-unselected;
|
||||
-fx-text-fill: -fixed-log-toggle-unselected;
|
||||
}
|
||||
@@ -1348,6 +1406,10 @@
|
||||
-fx-background-color: -fixed-log-info;
|
||||
}
|
||||
|
||||
.log-window-list-cell {
|
||||
-fx-background-color: -fixed-log-info;
|
||||
}
|
||||
|
||||
.log-window-list-cell:debug {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-debug;
|
||||
@@ -1403,14 +1465,6 @@
|
||||
-fx-stroke-width: 3.0;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-jfx-radius: 20;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.second-spinner {
|
||||
-jfx-radius: 30;
|
||||
}
|
||||
@@ -1860,32 +1914,51 @@
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Line Button *
|
||||
* Line Component *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
|
||||
.line-select-button .svg {
|
||||
-fx-opacity: 1;
|
||||
.line-component .line-component-container {
|
||||
-fx-padding: 10 16 10 16;
|
||||
}
|
||||
|
||||
.line-select-button .svg:disabled {
|
||||
.line-component.no-padding .line-component-container {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.line-component:large-title .title-label {
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.line-button .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .title {
|
||||
-fx-font-size: 13px;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .title {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .subtitle {
|
||||
.line-button .trailing-label {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .subtitle {
|
||||
.line-button .trailing-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.line-button .trailing-icon .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .title-label {
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.line-toggle-button .jfx-toggle-button {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
@@ -118,11 +118,11 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
|
||||
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
|
||||
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
|
||||
\n\
|
||||
If the website fails to load, please open %s manually in your browser.\n\
|
||||
\n\
|
||||
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
|
||||
account.methods.microsoft.methods.device=Log In with QR Code
|
||||
account.methods.microsoft.methods.device.hint=Scan QR code or visit <a href="%s">%s</a> to complete login, enter <b>%s</b> in the opened page.
|
||||
account.methods.microsoft.methods.browser=Log In via Browser
|
||||
account.methods.microsoft.methods.browser.hint=Click the "Log in" button or <a href="%s">copy the link</a> and paste it into the browser to log in.
|
||||
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
|
||||
account.methods.microsoft.profile=Account Profile
|
||||
account.methods.microsoft.purchase=Buy Minecraft
|
||||
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
|
||||
@@ -141,7 +141,6 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play
|
||||
\n\
|
||||
This option is for advanced users only. We do not recommend changing this option unless you know what you are doing.
|
||||
account.methods.offline.uuid.malformed=Invalid format.
|
||||
account.methods.forgot_password=Forgot Password
|
||||
account.methods.ban_query=Ban Query
|
||||
account.missing=No Accounts
|
||||
account.missing.add=Click here to add one.
|
||||
@@ -368,7 +367,6 @@ download.provider.balanced.desc=Balanced, but may not be the latest
|
||||
download.provider.mirror=From Mirror
|
||||
download.provider.mirror.desc=Fast, but may not be the latest
|
||||
download.java=Downloading Java
|
||||
download.java.override=This Java version already exists. Do you want to uninstall and reinstall it?
|
||||
download.java.process=Java Download Process
|
||||
download.javafx=Downloading dependencies for the launcher...
|
||||
download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\
|
||||
@@ -390,12 +388,18 @@ exception.ssl_handshake=Failed to establish SSL connection because the SSL certi
|
||||
exception.dns.pollution=Failed to establish an SSL connection. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service.
|
||||
|
||||
extension.bat=Windows Batch File
|
||||
extension.mod=Mod File
|
||||
extension.png=Image File
|
||||
extension.ps1=Windows PowerShell Script
|
||||
extension.sh=Shell Script
|
||||
extension.command=macOS Shell Script
|
||||
|
||||
extension.datapack=Datapack Archive
|
||||
extension.mod=Mod File
|
||||
extension.modloader.installer=Mod Loader Installer
|
||||
extension.resourcepack=Resource Pack Archive
|
||||
extension.schematic=Schematic File
|
||||
extension.world=World Archive
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
|
||||
fatal.javafx.incompatible=Missing JavaFX environment.\n\
|
||||
Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\
|
||||
@@ -459,7 +463,7 @@ folder.logs=Logs
|
||||
folder.mod=Mods
|
||||
folder.resourcepacks=Resource Packs
|
||||
folder.shaderpacks=Shader Packs
|
||||
folder.saves=Saves
|
||||
folder.saves=Worlds
|
||||
folder.schematics=Schematics
|
||||
folder.screenshots=Screenshots
|
||||
folder.world=World Directory
|
||||
@@ -472,7 +476,7 @@ game.crash.reason.analyzing=Analyzing...
|
||||
game.crash.reason.multiple=Several reasons detected:\n\n
|
||||
game.crash.reason.block=The game crashed because of a block in the world.\n\
|
||||
\n\
|
||||
You can try removing this block using MCEdit or deleting the mod that added it.\n\
|
||||
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
|
||||
\n\
|
||||
Block Type: %1$s\n\
|
||||
Location: %2$s
|
||||
@@ -488,7 +492,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
|
||||
Each mod can only be installed once. Please delete the duplicate mod and try again.
|
||||
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
|
||||
\n\
|
||||
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
|
||||
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
|
||||
\n\
|
||||
Entity Type: %1$s\n\
|
||||
Location: %2$s
|
||||
@@ -733,10 +737,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Installing %s
|
||||
install.installer.install_offline=Install/Update from Local File
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine installer
|
||||
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
|
||||
install.installer.install_online=Online Install
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt, and LiteLoader.
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt, and LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Not installed
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -756,17 +759,18 @@ install.new_game.malformed=Invalid name.
|
||||
install.select=Choose operation
|
||||
install.success=Successfully installed.
|
||||
|
||||
java.add=Add Java
|
||||
java.add=Add
|
||||
java.add.failed=This Java is invalid or incompatible with the current platform.
|
||||
java.disable=Disable Java
|
||||
java.disable.confirm=Are you sure you want to disable this Java?
|
||||
java.disabled.management=Disabled Java
|
||||
java.disabled.management.remove=Remove this Java from the list
|
||||
java.disabled.management.restore=Re-enable this Java
|
||||
java.download=Download Java
|
||||
java.download=Download
|
||||
java.download.banshanjdk-8=Download Banshan JDK 8
|
||||
java.download.load_list.failed=Failed to load version list
|
||||
java.download.more=More Java distributions
|
||||
java.download.title=Download Java
|
||||
java.download.prompt=Please choose the Java version you want to download:
|
||||
java.download.distribution=Distribution
|
||||
java.download.version=Version
|
||||
@@ -1063,9 +1067,10 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add=Add Mod
|
||||
mods.add=Add
|
||||
mods.add.failed=Failed to add mod %s.
|
||||
mods.add.success=%s was successfully added.
|
||||
mods.add.title=Choose mod file you want to add
|
||||
mods.broken_dependency.title=Broken dependency
|
||||
mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source.
|
||||
mods.category=Category
|
||||
@@ -1082,7 +1087,6 @@ mods.check_updates.failed_download=Failed to download some files.
|
||||
mods.check_updates.file=File
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target Version
|
||||
mods.choose_mod=Choose mod
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=Built-in Dependencies (Already packaged in the mod file by the author. No need to download separately)
|
||||
mods.dependency.optional=Optional Dependencies (If missing, the game will run normally, but the mod features may be missing)
|
||||
@@ -1104,7 +1108,7 @@ mods.mcmod.page=MCMod Page
|
||||
mods.mcmod.search=Search in MCMod
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Name
|
||||
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or LiteLoader) first to manage your mods!
|
||||
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Legacy Fabric, Quilt, or LiteLoader) first to manage your mods!
|
||||
mods.restore=Restore
|
||||
mods.url=Official Page
|
||||
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
|
||||
@@ -1127,9 +1131,8 @@ nbt.save.failed=Failed to save file
|
||||
nbt.title=View File - %s
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Install Datapack
|
||||
datapack.choose_datapack=Choose datapack to import
|
||||
datapack.extension=Datapack
|
||||
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
|
||||
|
||||
@@ -1138,9 +1141,13 @@ web.open_in_browser=Do you want to open this address in a browser:\n%s
|
||||
web.view_in_browser=View all in browser
|
||||
|
||||
world=Worlds
|
||||
world.add=Add World
|
||||
world.add=Add
|
||||
world.add.already_exists=This world already exists.
|
||||
world.add.failed=Failed to add this world: %s
|
||||
world.add.invalid=Failed to parse the world.
|
||||
world.add.title=Choose world archive you want to add
|
||||
world.backup=World Backup
|
||||
world.backup.create.new_one=Create New Backup
|
||||
world.backup.create.new_one=New Backup
|
||||
world.backup.create.failed=Failed to create backup.\n%s
|
||||
world.backup.create.success=Successfully created a new backup: %s
|
||||
world.backup.delete=Delete this backup
|
||||
@@ -1161,13 +1168,12 @@ world.datapack=Datapacks
|
||||
world.datetime=Last played on %s
|
||||
world.delete=Delete the World
|
||||
world.delete.failed=Failed to delete world.\n%s
|
||||
world.download=Download World
|
||||
world.download=Download
|
||||
world.download.title=Download World - %1s
|
||||
world.export=Export the World
|
||||
world.export.title=Choose the directory for this exported world
|
||||
world.export.location=Save As
|
||||
world.export.wizard=Export World "%s"
|
||||
world.extension=World Archive
|
||||
world.game_version=Game Version
|
||||
world.icon=World Icon
|
||||
world.icon.change=Change world icon
|
||||
@@ -1178,10 +1184,6 @@ world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of
|
||||
world.icon.change.succeed.toast=Successfully updated the world icon.
|
||||
world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft.
|
||||
world.icon.choose.title=Select world icon
|
||||
world.import.already_exists=This world already exists.
|
||||
world.import.choose=Choose world archive you want to import
|
||||
world.import.failed=Failed to import this world: %s
|
||||
world.import.invalid=Failed to parse the world.
|
||||
world.info=World Information
|
||||
world.info.basic=Basic Information
|
||||
world.info.allow_cheats=Allow Commands/Cheats
|
||||
@@ -1250,6 +1252,7 @@ repositories.chooser.title=Choose download source for JavaFX
|
||||
|
||||
resourcepack=Resource Packs
|
||||
resourcepack.add=Add
|
||||
resourcepack.add.title=Choose resource pack archive you want to add
|
||||
resourcepack.manage=Resource Packs
|
||||
resourcepack.download=Download
|
||||
resourcepack.add.failed=Failed to add resource pack
|
||||
@@ -1259,10 +1262,11 @@ resourcepack.download.title=Download Resource Pack - %1s
|
||||
reveal.in_file_manager=Reveal in File Manager
|
||||
|
||||
schematics=Schematics
|
||||
schematics.add=Add Schematic Files
|
||||
schematics.add=Add
|
||||
schematics.add.failed=Failed to add schematic files
|
||||
schematics.add.title=Choose schematic file you want to import
|
||||
schematics.back_to=Back to "%s"
|
||||
schematics.create_directory=Create Directory
|
||||
schematics.create_directory=New Directory
|
||||
schematics.create_directory.prompt=Please enter the new directory name
|
||||
schematics.create_directory.failed=Failed to create directory
|
||||
schematics.create_directory.failed.already_exists=Directory already exists
|
||||
@@ -1398,9 +1402,9 @@ settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=Manage
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose the working directory
|
||||
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, saves, and mods in a separate directory.\n\
|
||||
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, worlds, and mods in a separate directory.\n\
|
||||
\n\
|
||||
It is recommended to enable this option to avoid mod conflicts, but you will need to move your saves manually.
|
||||
It is recommended to enable this option to avoid mod conflicts, but you will need to move your worlds manually.
|
||||
|
||||
settings.icon=Icon
|
||||
|
||||
@@ -1627,7 +1631,7 @@ version.manage=All Instances
|
||||
version.manage.clean=Delete Log Files
|
||||
version.manage.clean.tooltip=Delete the files in "logs" and "crash-reports" directories.
|
||||
version.manage.duplicate=Duplicate Instance
|
||||
version.manage.duplicate.duplicate_save=Duplicate Saves
|
||||
version.manage.duplicate.duplicate_save=Duplicate Worlds
|
||||
version.manage.duplicate.prompt=Enter New Instance Name
|
||||
version.manage.duplicate.confirm=The duplicated instance will have a copy of all files in the instance directory (".minecraft/versions/<instance name>"), with an isolated working directory and settings.
|
||||
version.manage.manage=Edit Instance
|
||||
@@ -1635,7 +1639,7 @@ version.manage.manage.title=Edit Instance - %1s
|
||||
version.manage.redownload_assets_index=Update Game Assets
|
||||
version.manage.remove=Delete Instance
|
||||
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s".
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its worlds and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove.failed=Failed to delete the instance. Some files might be in use.
|
||||
version.manage.remove_assets=Delete All Assets
|
||||
version.manage.remove_libraries=Delete All Libraries
|
||||
|
||||
@@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID هو معرف فريد للاعبي Minec
|
||||
\n\
|
||||
هذا الخيار للمستخدمين المتقدمين فقط. لا نوصي بتغيير هذا الخيار ما لم تعرف ما تفعله.
|
||||
account.methods.offline.uuid.malformed=تنسيق غير صالح.
|
||||
account.methods.forgot_password=نسيت كلمة المرور
|
||||
account.methods.ban_query=استعلام الحظر
|
||||
account.missing=لا توجد حسابات
|
||||
account.missing.add=انقر هنا لإضافة واحد.
|
||||
@@ -341,7 +340,6 @@ download.provider.official=من المصادر الرسمية
|
||||
download.provider.balanced=من الأسرع المتاح
|
||||
download.provider.mirror=من المرآة
|
||||
download.java=تنزيل Java
|
||||
download.java.override=هذا الإصدار من Java موجود بالفعل. هل تريد إلغاء التثبيت وإعادة التثبيت؟
|
||||
download.java.process=عملية تنزيل Java
|
||||
download.javafx=تنزيل التبعيات للمشغل...
|
||||
download.javafx.notes=نحن نقوم حالياً بتنزيل تبعيات HMCL من الإنترنت.\n\
|
||||
@@ -363,11 +361,14 @@ exception.ssl_handshake=فشل إنشاء اتصال SSL لأن شهادة SSL
|
||||
exception.dns.pollution=فشل إنشاء اتصال SSL. قد تكون هناك مشكلة في تحليل أسماء النطاقات (DNS). يرجى محاولة تغيير خادم DNS أو استخدام خدمة وكيل.
|
||||
|
||||
extension.bat=ملف Batch لـ Windows
|
||||
extension.mod=ملف التعديل
|
||||
extension.png=ملف صورة
|
||||
extension.ps1=نص PowerShell لـ Windows
|
||||
extension.sh=نص Shell
|
||||
|
||||
extension.datapack=حزمة بيانات
|
||||
extension.mod=ملف التعديل
|
||||
extension.world=أرشيف العالم
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher لا يمكنه إنشاء دليل HMCL (%s). يرجى نقل HMCL إلى موقع آخر وإعادة فتحه.
|
||||
fatal.javafx.incompatible=بيئة JavaFX مفقودة.\n\
|
||||
Hello Minecraft! Launcher لا يمكنه تثبيت JavaFX تلقائياً على Java <11.\n\
|
||||
@@ -431,7 +432,6 @@ folder.logs=السجلات
|
||||
folder.mod=التعديلات
|
||||
folder.resourcepacks=حزم الموارد
|
||||
folder.shaderpacks=حزم الظلال
|
||||
folder.saves=الحفظ
|
||||
folder.schematics=المخططات
|
||||
folder.screenshots=لقطات الشاشة
|
||||
folder.world=دليل العالم
|
||||
@@ -442,12 +442,6 @@ game.crash.info=معلومات التعطل
|
||||
game.crash.reason=سبب التعطل
|
||||
game.crash.reason.analyzing=جاري التحليل...
|
||||
game.crash.reason.multiple=تم اكتشاف عدة أسباب:\n\n
|
||||
game.crash.reason.block=تعطلت اللعبة بسبب كتلة في العالم.\n\
|
||||
\n\
|
||||
يمكنك محاولة إزالة هذه الكتلة باستخدام MCEdit أو حذف التعديل الذي أضافها.\n\
|
||||
\n\
|
||||
نوع الكتلة: %1$s\n\
|
||||
الموقع: %2$s
|
||||
game.crash.reason.bootstrap_failed=تعطلت اللعبة بسبب التعديل "%1$s".\n\
|
||||
\n\
|
||||
يمكنك محاولة حذفه أو تحديثه.
|
||||
@@ -458,12 +452,6 @@ game.crash.reason.duplicated_mod=لا يمكن للعبة الاستمرار ف
|
||||
%2$s\n\
|
||||
\n\
|
||||
يمكن تثبيت كل تعديل مرة واحدة فقط. يرجى حذف التعديل المكرر والمحاولة مرة أخرى.
|
||||
game.crash.reason.entity=تعطلت اللعبة بسبب كيان في العالم.\n\
|
||||
\n\
|
||||
يمكنك محاولة إزالة الكيان باستخدام MCEdit أو حذف التعديل الذي أضافه.\n\
|
||||
\n\
|
||||
نوع الكيان: %1$s\n\
|
||||
الموقع: %2$s
|
||||
game.crash.reason.modmixin_failure=تعطلت اللعبة لأن بعض التعديلات فشلت في الحقن.\n\
|
||||
\n\
|
||||
هذا يعني عموماً أن التعديل به خطأ أو غير متوافق مع البيئة الحالية.\n\
|
||||
@@ -703,7 +691,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=غير متوافق مع %s
|
||||
install.installer.install=تثبيت %s
|
||||
install.installer.install_offline=التثبيت/التحديث من ملف محلي
|
||||
install.installer.install_offline.extension=مثبت (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=ندعم استخدام مثبت (Neo)Forge و Cleanroom و OptiFine المحلي.
|
||||
install.installer.install_online=التثبيت عبر الإنترنت
|
||||
install.installer.install_online.tooltip=ندعم حالياً Forge و NeoForge و Cleanroom و OptiFine و Fabric و Quilt و LiteLoader.
|
||||
@@ -726,14 +713,12 @@ install.new_game.malformed=اسم غير صالح.
|
||||
install.select=اختر العملية
|
||||
install.success=تم التثبيت بنجاح.
|
||||
|
||||
java.add=إضافة Java
|
||||
java.add.failed=هذا Java غير صالح أو غير متوافق مع المنصة الحالية.
|
||||
java.disable=تعطيل Java
|
||||
java.disable.confirm=هل أنت متأكد من أنك تريد تعطيل هذا Java؟
|
||||
java.disabled.management=Java المعطل
|
||||
java.disabled.management.remove=إزالة هذا Java من القائمة
|
||||
java.disabled.management.restore=إعادة تفعيل هذا Java
|
||||
java.download=تنزيل Java
|
||||
java.download.banshanjdk-8=تنزيل Banshan JDK 8
|
||||
java.download.load_list.failed=فشل تحميل قائمة الإصدارات
|
||||
java.download.more=المزيد من توزيعات Java
|
||||
@@ -1032,7 +1017,6 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=التعديلات
|
||||
mods.add=إضافة تعديل
|
||||
mods.add.failed=فشل إضافة التعديل %s.
|
||||
mods.add.success=تمت إضافة %s بنجاح.
|
||||
mods.broken_dependency.title=تبعية معطلة
|
||||
@@ -1050,7 +1034,6 @@ mods.check_updates.file=ملف
|
||||
mods.check_updates.source=المصدر
|
||||
mods.check_updates.target_version=الإصدار المستهدف
|
||||
mods.check_updates.update=تحديث
|
||||
mods.choose_mod=اختر تعديلاً
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=تبعيات مدمجة (معبأة بالفعل في ملف التعديل من قبل المطور. لا حاجة للتنزيل بشكل منفصل)
|
||||
mods.dependency.optional=تبعيات اختيارية (إذا كانت مفقودة، ستعمل اللعبة بشكل طبيعي، ولكن قد تكون ميزات التعديل مفقودة)
|
||||
@@ -1087,9 +1070,6 @@ nbt.save.failed=فشل حفظ الملف
|
||||
nbt.title=عرض الملف - %s
|
||||
|
||||
datapack=حزم البيانات
|
||||
datapack.add=تثبيت حزمة بيانات
|
||||
datapack.choose_datapack=اختر حزمة بيانات للاستيراد
|
||||
datapack.extension=حزمة بيانات
|
||||
datapack.title=عالم [%s] - حزم البيانات
|
||||
|
||||
web.failed=فشل تحميل الصفحة
|
||||
@@ -1097,9 +1077,11 @@ web.open_in_browser=هل تريد فتح هذا العنوان في المتصف
|
||||
web.view_in_browser=عرض الكل في المتصفح
|
||||
|
||||
world=العوالم
|
||||
world.add=إضافة عالم
|
||||
world.add.already_exists=هذا العالم موجود بالفعل.
|
||||
world.add.title=اختر أرشيف العالم الذي تريد استيراده
|
||||
world.add.failed=فشل استيراد هذا العالم: %s
|
||||
world.add.invalid=فشل تحليل العالم.
|
||||
world.backup=نسخ احتياطي للعالم
|
||||
world.backup.create.new_one=إنشاء نسخة احتياطية جديدة
|
||||
world.backup.create.failed=فشل إنشاء النسخة الاحتياطية.\n%s
|
||||
world.backup.create.success=تم إنشاء نسخة احتياطية جديدة بنجاح: %s
|
||||
world.backup.delete=حذف هذه النسخة الاحتياطية
|
||||
@@ -1113,18 +1095,12 @@ world.datapack=حزم البيانات
|
||||
world.datetime=آخر لعب في %s
|
||||
world.delete=حذف العالم
|
||||
world.delete.failed=فشل حذف العالم.\n%s
|
||||
world.download=تنزيل عالم
|
||||
world.download.title=تنزيل عالم - %1s
|
||||
world.export=تصدير العالم
|
||||
world.export.title=اختر الدليل لهذا العالم المُصدر
|
||||
world.export.location=حفظ باسم
|
||||
world.export.wizard=تصدير العالم "%s"
|
||||
world.extension=أرشيف العالم
|
||||
world.game_version=إصدار اللعبة
|
||||
world.import.already_exists=هذا العالم موجود بالفعل.
|
||||
world.import.choose=اختر أرشيف العالم الذي تريد استيراده
|
||||
world.import.failed=فشل استيراد هذا العالم: %s
|
||||
world.import.invalid=فشل تحليل العالم.
|
||||
world.info=معلومات العالم
|
||||
world.info.basic=معلومات أساسية
|
||||
world.info.allow_cheats=السماح بالأوامر/الغش
|
||||
@@ -1191,10 +1167,8 @@ resourcepack.download.title=تنزيل حزمة موارد - %1s
|
||||
reveal.in_file_manager=إظهار في مدير الملفات
|
||||
|
||||
schematics=المخططات
|
||||
schematics.add=إضافة ملفات مخططات
|
||||
schematics.add.failed=فشل إضافة ملفات المخططات
|
||||
schematics.back_to=العودة إلى "%s"
|
||||
schematics.create_directory=إنشاء دليل
|
||||
schematics.create_directory.prompt=يرجى إدخال اسم الدليل الجديد
|
||||
schematics.create_directory.failed=فشل إنشاء الدليل
|
||||
schematics.create_directory.failed.already_exists=الدليل موجود بالفعل
|
||||
@@ -1324,9 +1298,6 @@ settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=إدارة
|
||||
settings.game.working_directory=دليل العمل
|
||||
settings.game.working_directory.choose=اختر دليل العمل
|
||||
settings.game.working_directory.hint=قم بتفعيل خيار "معزول" في "دليل العمل" للسماح للنسخة الحالية بتخزين إعداداتها وحفظها وتعديلاتها في دليل منفصل.\n\
|
||||
\n\
|
||||
يوصى بتفعيل هذا الخيار لتجنب تعارضات التعديلات، ولكن ستحتاج إلى نقل حفظك يدويًا.
|
||||
|
||||
settings.icon=الأيقونة
|
||||
|
||||
@@ -1539,7 +1510,6 @@ version.manage=جميع النسخ
|
||||
version.manage.clean=حذف ملفات السجل
|
||||
version.manage.clean.tooltip=حذف الملفات في أدلة "logs" و "crash-reports".
|
||||
version.manage.duplicate=تكرار النسخة
|
||||
version.manage.duplicate.duplicate_save=تكرار الحفظ
|
||||
version.manage.duplicate.prompt=أدخل اسم النسخة الجديد
|
||||
version.manage.duplicate.confirm=ستحتوي النسخة المكررة على نسخة من جميع الملفات في دليل النسخة (".minecraft/versions/<اسم النسخة>")، مع دليل عمل وإعدادات معزولة.
|
||||
version.manage.manage=تحرير النسخة
|
||||
@@ -1547,7 +1517,6 @@ version.manage.manage.title=تحرير النسخة - %1s
|
||||
version.manage.redownload_assets_index=تحديث أصول اللعبة
|
||||
version.manage.remove=حذف النسخة
|
||||
version.manage.remove.confirm.trash=هل أنت متأكد من أنك تريد إزالة النسخة "%1$s"؟ لا يزال بإمكانك العثور على ملفاتها في سلة المحذوفات باسم "%2$s".
|
||||
version.manage.remove.confirm.independent=نظرًا لأن هذه النسخة مخزنة في دليل معزول، فإن حذفها سيحذف أيضًا حفظها وبيانات أخرى. هل لا تزال تريد حذف النسخة "%s"؟
|
||||
version.manage.remove.failed=خطأ في حذف النسخة. قد تكون بعض الملفات لازالت في الإستخدام.
|
||||
version.manage.remove_assets=حذف جميع الأصول
|
||||
version.manage.remove_libraries=حذف جميع المكتبات
|
||||
|
||||
@@ -123,7 +123,6 @@ account.methods.offline.uuid.hint=UUID es un identificador único para los jugad
|
||||
\n\
|
||||
Esta opción es sólo para usuarios avanzados. No recomendamos editar esta opción a menos que sepas lo que estás haciendo.
|
||||
account.methods.offline.uuid.malformed=Formato no válido.
|
||||
account.methods.forgot_password=Olvidé mi contraseña
|
||||
account.methods.ban_query=Consulta de bloqueo de cuentas
|
||||
account.missing=No hay cuentas
|
||||
account.missing.add=Haga clic aquí para añadir una.
|
||||
@@ -346,7 +345,6 @@ download.provider.official=De fuentes oficiales
|
||||
download.provider.balanced=De la fuente más rápida disponible
|
||||
download.provider.mirror=Desde espejo
|
||||
download.java=Descargando Java
|
||||
download.java.override=Esta versión de Java ya existe. ¿Desea desinstalarla y volver a instalarla?
|
||||
download.java.process=Proceso de descarga Java
|
||||
download.javafx=Descargando dependencias para el launcher...
|
||||
download.javafx.notes=Estamos descargando dependencias para HMCL desde Internet.\n\
|
||||
@@ -365,11 +363,14 @@ exception.ssl_handshake=No se pudo establecer una conexión SSL porque falta el
|
||||
exception.dns.pollution=No se pudo establecer una conexión SSL. Es posible que la resolución DNS sea incorrecta. Por favor, intente cambiar el servidor DNS o usar un servicio proxy.
|
||||
|
||||
extension.bat=Archivo por lotes de Windows
|
||||
extension.mod=Archivo mod
|
||||
extension.png=Archivo de imagen
|
||||
extension.ps1=Windows PowerShell Script
|
||||
extension.sh=Shell Script
|
||||
|
||||
extension.datapack=Paquete de datos
|
||||
extension.mod=Archivo mod
|
||||
extension.world=Archivo del mundo
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=El lanzador no puede crear el directorio HMCL (%s). Por favor, mueva el lanzador a otra ubicación y vuelva a abrirlo.
|
||||
fatal.javafx.incompatible=No se encontró un entorno JavaFX.\n\
|
||||
Hello Minecraft! Launcher no puede instalar automáticamente JavaFX con versiones de Java inferiores a la 11.\n\
|
||||
@@ -447,12 +448,6 @@ game.crash.info=Información de colgado
|
||||
game.crash.reason=Motivo del colgado
|
||||
game.crash.reason.analyzing=Analizando...
|
||||
game.crash.reason.multiple=Se han detectado varias razones:\n\n
|
||||
game.crash.reason.block=El juego se ha colgado debido a un bloqueo en el mundo.\n\
|
||||
\n\
|
||||
Puedes intentar eliminar este bloque usando MCEdit o borrando el mod que lo añadió.\n\
|
||||
\n\
|
||||
Tipo de bloque: %1$s\n\
|
||||
Ubicación: %2$s
|
||||
game.crash.reason.bootstrap_failed=El juego se ha colgado debido al mod «%1$s».\n\
|
||||
\n\
|
||||
Puedes intentar borrarlo o actualizarlo.
|
||||
@@ -466,12 +461,6 @@ game.crash.reason.duplicated_mod=El juego no puede iniciarse debido a la existen
|
||||
%2$s\n\
|
||||
\n\
|
||||
Cada mod sólo puede instalarse una vez, por favor, elimine el mod duplicado e inténtelo de nuevo.
|
||||
game.crash.reason.entity=El juego se ha colgado debido a una entidad en el mundo.\n\
|
||||
\n\
|
||||
Puedes intentar eliminar la entidad usando MCEdit o borrando el mod que la añadió.\n\
|
||||
\n\
|
||||
Tipo de entidad: %1$s\n\
|
||||
Ubicación: %2$s
|
||||
game.crash.reason.fabric_version_0_12=La versión de Fabric Loader 0.12 o posterior es incompatible con los mods instalados actualmente. Necesitas bajarlo a la versión 0.11.7.
|
||||
game.crash.reason.fabric_warnings=El cargador de Fabric Loader ha advertido:\n\
|
||||
\n\
|
||||
@@ -710,10 +699,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible con %s
|
||||
install.installer.install=Instalando %s
|
||||
install.installer.install_offline=Instalar/actualizar desde archivo local
|
||||
install.installer.install_offline.extension=Instalador de (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Instalación en línea
|
||||
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine.
|
||||
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader y OptiFine.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=No está instalado
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -733,14 +721,12 @@ install.new_game.malformed=Nombre no válido.
|
||||
install.select=Elegir una operación
|
||||
install.success=Instalado con éxito.
|
||||
|
||||
java.add=Añadir Java
|
||||
java.add.failed=Este Java no es válido o es incompatible con la plataforma actual.
|
||||
java.disable=Deshabilitar este Java
|
||||
java.disable.confirm=¿Está seguro de que desea desactivar este Java?
|
||||
java.disabled.management=Java desactivado
|
||||
java.disabled.management.remove=Eliminar este Java de la lista
|
||||
java.disabled.management.restore=Activar este Java
|
||||
java.download=Descargar Java
|
||||
java.download.banshanjdk-8=Descargar Banshan JDK 8
|
||||
java.download.load_list.failed=No se ha podido cargar la lista de versiones
|
||||
java.download.more=Más distribuciones de Java
|
||||
@@ -1039,7 +1025,6 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add=Añadir mods
|
||||
mods.add.failed=No se ha podido añadir el mod %s.
|
||||
mods.add.success=%s se ha añadido correctamente.
|
||||
mods.broken_dependency.title=Dependencia rota
|
||||
@@ -1058,7 +1043,6 @@ mods.check_updates.failed_download=No se han podido descargar algunos de los arc
|
||||
mods.check_updates.file=Archivo
|
||||
mods.check_updates.source=Fuente
|
||||
mods.check_updates.target_version=Versión de destino
|
||||
mods.choose_mod=Elige un mod
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=Dependencias incorporadas (Already packaged in the mod file by the author. No need to download separately)
|
||||
mods.dependency.optional=Dependencias opcionales (If missing, the game will run normally, but the mod features may be missing)
|
||||
@@ -1080,7 +1064,7 @@ mods.mcmod.page=Página de MCMod
|
||||
mods.mcmod.search=Búsqueda en MCMod
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Nombre
|
||||
mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, Fabric, Quilt o LiteLoader) para gestionar tus mods!
|
||||
mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, Fabric, Legacy Fabric, Quilt o LiteLoader) para gestionar tus mods!
|
||||
mods.restore=Restaurar
|
||||
mods.url=Página oficial
|
||||
mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar?
|
||||
@@ -1093,9 +1077,6 @@ nbt.save.failed=No se ha podido guardar el archivo
|
||||
nbt.title=Ver archivo - %s
|
||||
|
||||
datapack=Paquetes de datos
|
||||
datapack.add=Instalar paquetes de datos
|
||||
datapack.choose_datapack=Elija el paquete de datos que desea importar
|
||||
datapack.extension=Paquete de datos
|
||||
datapack.title=Mundo [%s] - Paquetes de datos
|
||||
|
||||
web.failed=No se ha podido cargar la página
|
||||
@@ -1103,9 +1084,11 @@ web.open_in_browser=Desea abrir esta dirección en un navegador:\n%s
|
||||
web.view_in_browser=Ver en navegador
|
||||
|
||||
world=Mundos
|
||||
world.add=Añadir mundo
|
||||
world.add.already_exists=Este mundo ya existe.
|
||||
world.add.title=Elija el archivo de mundo que desea importar
|
||||
world.add.failed=No se ha podido importar este mundo: %s
|
||||
world.add.invalid=No se ha podido analizar el mundo.
|
||||
world.backup=Copia de seguridad
|
||||
world.backup.create.new_one=Crear uno nuevo
|
||||
world.backup.create.failed=No se ha podido crear la copia de seguridad.\n%s
|
||||
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
|
||||
world.backup.delete=Eliminar esta copia de seguridad
|
||||
@@ -1122,18 +1105,12 @@ world.datapack=Paquetes de datos
|
||||
world.datetime=Jugado por última vez en %s
|
||||
world.delete=Eliminar este mundo
|
||||
world.delete.failed=No se pudo eliminar el mundo.\n%s
|
||||
world.download=Descargar Mundo
|
||||
world.download.title=Descargar mundo - %1s
|
||||
world.export=Exportar el mundo
|
||||
world.export.title=Elija el directorio para este mundo exportado
|
||||
world.export.location=Guardar como
|
||||
world.export.wizard=Exportar Mundo %s
|
||||
world.extension=Archivo del mundo
|
||||
world.game_version=Versión del juego
|
||||
world.import.already_exists=Este mundo ya existe.
|
||||
world.import.choose=Elija el archivo de mundo que desea importar
|
||||
world.import.failed=No se ha podido importar este mundo: %s
|
||||
world.import.invalid=No se ha podido analizar el mundo.
|
||||
world.info=Información del mundo
|
||||
world.info.basic=Información básica
|
||||
world.info.allow_cheats=Permitir comandos/trucos
|
||||
@@ -1200,10 +1177,8 @@ resourcepack.download.title=Descargar paquete de recursos - %1s
|
||||
reveal.in_file_manager=Mostrar en el administrador de archivos
|
||||
|
||||
schematics=Esquemas
|
||||
schematics.add=Añadir archivos de esquema
|
||||
schematics.add.failed=No se han podido añadir archivos de esquema
|
||||
schematics.back_to=Atrás a «%s»
|
||||
schematics.create_directory=Crear directorio
|
||||
schematics.create_directory.prompt=Introduzca el nuevo nombre del directorio
|
||||
schematics.create_directory.failed=No se ha podido crear el directorio
|
||||
schematics.create_directory.failed.already_exists=El directorio ya existe
|
||||
@@ -1332,9 +1307,6 @@ settings.game.java_directory.version=Especifique la versión de Java
|
||||
settings.game.management=Gestionar
|
||||
settings.game.working_directory=Directorio de trabajo
|
||||
settings.game.working_directory.choose=Elija el directorio de trabajo
|
||||
settings.game.working_directory.hint=Activar la opción «Aislar» en «Directorio de trabajo» para permitir que la instancia actual almacene sus configuraciones, mundos y mods en un directorio separado.\n\
|
||||
\n\
|
||||
Se recomienda activar esta opción para evitar conflictos con los mods, pero tendrás que mover tus guardados manualmente.
|
||||
|
||||
settings.icon=Icono
|
||||
|
||||
@@ -1472,7 +1444,6 @@ version.manage.manage.title=Editar instancia - %1s
|
||||
version.manage.redownload_assets_index=Actualizar activos del juego
|
||||
version.manage.remove=Eliminar instancia
|
||||
version.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s».
|
||||
version.manage.remove.confirm.independent=Dado que esta instancia está almacenada en un directorio aislado, al eliminarla también se eliminarán sus guardados y otros datos. ¿Aún quieres borrar la instancia %s?
|
||||
version.manage.remove_assets=Borrar todas las activos del juego
|
||||
version.manage.remove_libraries=Borrar todas las bibliotecas
|
||||
version.manage.rename=Renombrar instancia
|
||||
|
||||
@@ -91,7 +91,6 @@ account.methods.microsoft.birth=誕生日の設定を編集する方法...
|
||||
account.methods.microsoft.deauthorize=アカウントのバインドを解除
|
||||
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
|
||||
account.methods.microsoft.logging_in=ログイン...
|
||||
account.methods.forgot_password=パスワードをお忘れの方
|
||||
account.methods.microsoft.makegameidsettings=プロファイルを作成/プロフィール名を編集する
|
||||
account.methods.microsoft.profile=アカウントプロファイル..
|
||||
account.methods.microsoft.purchase=Minecraftを購入する
|
||||
@@ -315,11 +314,14 @@ exception.ssl_handshake=現在のJava仮想マシンに該当するSSL証明書
|
||||
exception.dns.pollution=SSL 接続を確立できませんでした。DNS 解決が正しくない可能性があります。DNS サーバーを変更するか、プロキシサービスを使用してみてください。
|
||||
|
||||
extension.bat=WindowsBatファイル
|
||||
extension.mod=Modファイル
|
||||
extension.png=画像ファイル
|
||||
extension.ps1=PowerShellスクリプト
|
||||
extension.sh=Bashスクリプト
|
||||
|
||||
extension.datapack=Datapack
|
||||
extension.mod=Modファイル
|
||||
extension.world=World zip
|
||||
|
||||
fatal.javafx.incompatible=アプリケーションは11未満の現在のJava環境でJavaFXにパッチを適用できません。\nこのアプリは、JDK11以降またはJavaFXがバンドルされたJDKを使用して実行してください。
|
||||
fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用している場合は、Oracle JRE 8にダウングレードするか、BellSoft Liberica FullJREをインストールしてください。\n他のOpenJDKビルドを使用している場合は、OpenJFXが含まれていることを確認してください。
|
||||
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
|
||||
@@ -338,7 +340,6 @@ folder.game=ゲームディレクトリ
|
||||
folder.mod=Mod
|
||||
folder.resourcepacks=リソースパック
|
||||
folder.shaderpacks=シェーダーパックフォルダー
|
||||
folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
@@ -347,13 +348,11 @@ game.crash.info=ゲームステータス
|
||||
game.crash.reason=クラッシュアナライザー
|
||||
game.crash.reason.analyzing=分析中..
|
||||
game.crash.reason.multiple=複数の理由が検出されました:\n\n
|
||||
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s\nブロックの場所:%2$s
|
||||
game.crash.reason.bootstrap_failed=mod %1$s がクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
|
||||
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod %1$s は構成ファイル %2$s を解析できません。
|
||||
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
|
||||
game.crash.reason.duplicated_mod=Mod %1$s が重複してインストールされているため、現在のゲームを継続して実行できません。\n%2$s\n各Modは1つしかインストールできませんので、余分なModを削除して再度お試しください。
|
||||
game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用できないため、現在のゲームを続行できません。\n問題を解決するために、このモジュールを削除または更新してみてください。
|
||||
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s\nエンティティの場所:%2$s
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
|
||||
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
|
||||
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
|
||||
@@ -440,7 +439,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=%s と互換性がありません
|
||||
install.installer.install=インストール %s
|
||||
install.installer.install_offline=ファイルからのインストール/更新
|
||||
install.installer.install_offline.extension=Forge / OptiFineインストーラー
|
||||
install.installer.install_offline.tooltip=Forge / OptiFineインストールファイルのインポートをサポートします
|
||||
install.installer.install_online=オンラインでインストール
|
||||
install.installer.install_online.tooltip=Fabric、Forge、OptiFine、LiteLoaderのインストールをサポートします。
|
||||
@@ -458,14 +456,12 @@ install.new_game.malformed=無効な名前
|
||||
install.select=操作を選択します
|
||||
install.success=正常にインストールされました
|
||||
|
||||
java.add=Javaの追加
|
||||
java.add.failed=このJavaは無効であるか、現在のプラットフォームと互換性がない。
|
||||
java.disable=無効化
|
||||
java.disable.confirm=本当にこのJavaを無効にしますか?
|
||||
java.disabled.management=無効なJava
|
||||
java.disabled.management.remove=このJavaをリストから削除する
|
||||
java.disabled.management.restore=有効化
|
||||
java.download=Javaをダウンロード
|
||||
java.download.load_list.failed=バージョンリストの読み込みに失敗しました
|
||||
java.download.more=その他のJavaディストリビューション
|
||||
java.download.prompt=ダウンロードしたいJavaのバージョンを選択してください:
|
||||
@@ -660,7 +656,6 @@ modrinth.category.datapack=Datapack
|
||||
modrinth.category.folia=Folia
|
||||
|
||||
mods=Mods
|
||||
mods.add=modを追加
|
||||
mods.add.failed=mods %s の追加に失敗しました。
|
||||
mods.add.success=mods %s が正常に追加されました。
|
||||
mods.category=Category
|
||||
@@ -673,7 +668,6 @@ mods.check_updates.failed_download=一部のファイルのダウンロードに
|
||||
mods.check_updates.file=ファイル
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target
|
||||
mods.choose_mod=modを選択してください
|
||||
mods.curseforge=CurseForge
|
||||
mods.disable=無効にする
|
||||
mods.download=Modのダウンロード
|
||||
@@ -692,28 +686,22 @@ mods.url=公式ページ
|
||||
mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか?
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=データパックをインストールします
|
||||
datapack.choose_datapack=インポートするデータパックのzipを選択してください
|
||||
datapack.extension=Datapack
|
||||
datapack.title=World %s -データパック
|
||||
|
||||
world=マップ
|
||||
world.add=マップを追加(.zip)
|
||||
world.add.already_exists=このマップはすでに存在しています。
|
||||
world.add.title=インポートするzipファイルを選択してください
|
||||
world.add.failed=このマップをインポートできません:%s
|
||||
world.add.invalid=無効なワールドzipファイル
|
||||
world.datapack=データパックの管理
|
||||
world.datetime=最終ゲーム時刻:%s
|
||||
world.delete=このマップを削除
|
||||
world.delete.failed=マップの削除に失敗しました\n%s
|
||||
world.download=ダウンロード
|
||||
world.export=このマップをエクスポートする
|
||||
world.export.title=保存する場所を選択してください
|
||||
world.export.location=エクスポート先
|
||||
world.export.wizard=エクスポートワールド: %s
|
||||
world.extension=World zip
|
||||
world.game_version=ゲームバージョン
|
||||
world.import.already_exists=このマップはすでに存在しています。
|
||||
world.import.choose=インポートするzipファイルを選択してください
|
||||
world.import.failed=このマップをインポートできません:%s
|
||||
world.import.invalid=無効なワールドzipファイル
|
||||
world.manage=マップ
|
||||
world.manage.title=マップ - %s
|
||||
world.name=マップ名
|
||||
@@ -955,7 +943,6 @@ version.manage.manage.title=バージョンの管理 - %1s
|
||||
version.manage.redownload_assets_index=ゲームアセットファイルの更新
|
||||
version.manage.remove=このバージョンを削除します
|
||||
version.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。
|
||||
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン %s を削除しますか?
|
||||
version.manage.remove_assets=すべてのゲームリソースファイルの削除
|
||||
version.manage.remove_libraries=ライブラリファイルを削除します
|
||||
version.manage.rename=このバージョンの名前を変更します
|
||||
|
||||
@@ -110,7 +110,6 @@ account.methods.microsoft.logging_in=方登入……
|
||||
account.methods.microsoft.makegameidsettings=添檔 / 書檔之名
|
||||
account.methods.microsoft.profile=纂戶簿訊息
|
||||
account.methods.microsoft.purchase=買礦藝
|
||||
account.methods.forgot_password=亡符節
|
||||
account.methods.ban_query=檢戶簿羈否
|
||||
account.methods.microsoft.snapshot.website=官網
|
||||
account.methods.offline=離綫之式
|
||||
@@ -345,7 +344,6 @@ download.provider.balanced.desc=均,容有舊
|
||||
download.provider.mirror=先別源
|
||||
download.provider.mirror.desc=至快,容有舊
|
||||
download.java=引爪哇
|
||||
download.java.override=爪哇是版既現,除而復裝諸?
|
||||
download.java.process=引爪哇
|
||||
download.javafx=甫引行需諸案……
|
||||
download.javafx.notes=方引 HMCL 所需諸案。\n擊「迭引源」以閲詳,并迭之。擊「罷」以止而退。\n誡:引之允滯,宜迭其源。
|
||||
@@ -361,11 +359,14 @@ exception.ssl_handshake=無築 SSL 鏈。爪哇缺證。宜改爪哇,抑制廢
|
||||
exception.dns.pollution=無築 SSL 鏈。或由 DNS 污染、解析有誤也。宜更 DNS 伺服,抑用代理以訪。\n君可求助於右上之鈕。
|
||||
|
||||
extension.bat=視窗角本
|
||||
extension.mod=改囊案
|
||||
extension.png=圖案
|
||||
extension.ps1=PowerShell 角本
|
||||
extension.sh=Bash 角本
|
||||
|
||||
extension.datapack=錄囊
|
||||
extension.mod=改囊案
|
||||
extension.world=生界緊囊
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=HMCL 不能立其案夾 (%s),宜稍遷而再啟之。\n君可求助於 https://docs.hmcl.net/help.html。
|
||||
fatal.javafx.incompatible=JavaFX 闕。\n既行於爪哇之次乎爪哇十一者,HMCL 不能自補之也。宜晉爪哇於十一抑後之者。\n君可求助於 https://docs.hmcl.net/help.html。
|
||||
fatal.javafx.incomplete=JavaFX 損。宜更爪哇,抑復置 OpenJFX。
|
||||
@@ -407,14 +408,14 @@ game.crash.feedback=<b>無錄是頁以示人!</b>宜擊<b>「導崩記」</b>
|
||||
game.crash.info=戲訊
|
||||
game.crash.reason=潰退之由
|
||||
game.crash.reason.analyzing=方求之焉……
|
||||
game.crash.reason.block=塊謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s
|
||||
game.crash.reason.block=塊謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
|
||||
game.crash.reason.bootstrap_failed=改囊「%1$s」謬而崩。\n或抑廢之。
|
||||
game.crash.reason.mixin_apply_mod_failed=「%1$s」不能旁嵌而崩。\n或廢之,或新之。
|
||||
game.crash.reason.config=置設之案謬而崩。\n改囊「%1$s」之置設「%2$s」有謬。
|
||||
game.crash.reason.multiple=其所由也衆:\n\n
|
||||
game.crash.reason.debug_crash=手崩也。\n戲事無誤,乃君致之!
|
||||
game.crash.reason.duplicated_mod=改囊「%1$s」復而崩。\n%2$s\n改囊不能再載。宜廢冗者。
|
||||
game.crash.reason.entity=實體謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s
|
||||
game.crash.reason.entity=實體謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
|
||||
game.crash.reason.fabric_version_0_12=改囊或弗見容於緞載之〇點一二與愈晉者。宜退乎〇點一一點七。
|
||||
game.crash.reason.fabric_warnings=緞報曰:\n%1$s
|
||||
game.crash.reason.file_already_exists=案「%1$s」既存而謬。\n誠以爲可去,宜另備而去之,然後復行。
|
||||
@@ -511,7 +512,6 @@ install.installer.game=礦藝
|
||||
install.installer.incompatible=與 %s 不兼也
|
||||
install.installer.install=裝 %s
|
||||
install.installer.install_offline=取私案而置
|
||||
install.installer.install_offline.extension= (新) 鍛/Cleanroom/OptiFine 裝者
|
||||
install.installer.install_offline.tooltip=可錄 (新) 鍛/Cleanroom/OptiFine 裝者
|
||||
install.installer.install_online=網取而置
|
||||
install.installer.install_online.tooltip=可置鍛、新鍛、Cleanroom、緞、褥、輕載與 OptiFine
|
||||
@@ -833,6 +833,7 @@ mods=改囊
|
||||
mods.add=增改囊
|
||||
mods.add.failed=增改囊「%s」未成。\n君可求助於右上之鈕。
|
||||
mods.add.success=增改囊 %s 畢。
|
||||
mods.add.title=擇改囊
|
||||
mods.broken_dependency.title=所依之壞者
|
||||
mods.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。
|
||||
mods.category=類
|
||||
@@ -849,7 +850,6 @@ mods.check_updates.failed_download=有引案未成
|
||||
mods.check_updates.file=案
|
||||
mods.check_updates.source=源
|
||||
mods.check_updates.target_version=將至之版
|
||||
mods.choose_mod=擇改囊
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=既存之相依改囊 (既以內於改囊案,無須他引)
|
||||
mods.dependency.optional=可選之相依改囊 (设若阙如,戲亦能行)
|
||||
@@ -885,8 +885,7 @@ nbt.title=覽案 - %s
|
||||
|
||||
datapack=錄囊
|
||||
datapack.add=增錄囊
|
||||
datapack.choose_datapack=擇須導入之錄囊緊囊
|
||||
datapack.extension=錄囊
|
||||
datapack.add.title=擇須導入之錄囊緊囊
|
||||
datapack.title=生界 [%s] - 錄囊
|
||||
|
||||
web.failed=載網葉未成
|
||||
@@ -895,6 +894,10 @@ web.view_in_browser=覽全誌于瀏網器
|
||||
|
||||
world=生界
|
||||
world.add=增生界
|
||||
world.add.already_exists=生界既存
|
||||
world.add.title=擇須導入之生界緊囊
|
||||
world.add.failed=不能錄生界:「%s」
|
||||
world.add.invalid=無可辨此緊囊
|
||||
world.backup=司備
|
||||
world.backup.create.new_one=建新備
|
||||
world.backup.create.failed=建備未成。\n%s
|
||||
@@ -917,12 +920,7 @@ world.export=錄出生界
|
||||
world.export.title=擇是生界之存處
|
||||
world.export.location=存至
|
||||
world.export.wizard=錄出生界「%s」
|
||||
world.extension=生界緊囊
|
||||
world.game_version=戲版
|
||||
world.import.already_exists=生界既存
|
||||
world.import.choose=擇須導入之生界緊囊
|
||||
world.import.failed=不能錄生界:「%s」
|
||||
world.import.invalid=無可辨此緊囊
|
||||
world.info=生界之訊
|
||||
world.info.basic=基訊
|
||||
world.info.allow_cheats=允令 (舞弊)
|
||||
|
||||
@@ -122,7 +122,6 @@ account.methods.offline.name.invalid=Для имени пользователя
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUID - это уникальный идентификатор игрового персонажа в Minecraft. Способ генерации UUID различается в разных лаунчерах игр. Изменение UUID на тот, который генерируется другим лаунчером гарантирует, что игровые блоки/предметы в рюкзаке вашего офлайн аккаунта останутся. Этот параметр предназначен для экспертов. Если вы не знаете что делаете, мы не советуем вам изменять этот параметр. Эта опция не требуется для присоединения к серверам.
|
||||
account.methods.offline.uuid.malformed=Недопустимый формат.
|
||||
account.methods.forgot_password=Забыли пароль
|
||||
account.methods.ban_query=Просмотреть аккаунт заблокирован
|
||||
account.missing=Нет аккаунтов
|
||||
account.missing.add=Нажмите здесь, чтобы добавить.
|
||||
@@ -343,7 +342,6 @@ download.provider.official=Из официальных источников
|
||||
download.provider.balanced=Из самых быстрых доступных
|
||||
download.provider.mirror=Из зеркала
|
||||
download.java=Скачивание Java
|
||||
download.java.override=Эта версия Java уже существует. Вы хотите удалить и установить ее заново?
|
||||
download.java.process=Процесс скачивания Java
|
||||
download.javafx=Скачивание зависимостей лаунчера...
|
||||
download.javafx.notes=Скачивание зависимостей для лаунчер из Интернета.\n\
|
||||
@@ -365,11 +363,14 @@ exception.ssl_handshake=Не удалось установить SSL-соеди
|
||||
exception.dns.pollution=Не удалось установить SSL‑соединение. Возможно, некорректно разрешаются DNS‑имена. Попробуйте сменить DNS‑сервер или воспользоваться прокси-службой.
|
||||
|
||||
extension.bat=Пакетный файл Windows
|
||||
extension.mod=Файл мода
|
||||
extension.png=Файл изображения
|
||||
extension.ps1=Сценарий Windows PowerShell
|
||||
extension.sh=Сценарий оболочки Bash
|
||||
|
||||
extension.datapack=Набор данных
|
||||
extension.mod=Файл мода
|
||||
extension.world=Архив мира
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Лаунчер не может создать папку HMCL (%s). Пожалуйста, переместите лаунчер в другое место и откройте его снова.
|
||||
fatal.javafx.incompatible=Отсутствует среда JavaFX.\n\
|
||||
Лаунчер не может автоматически установить JavaFX на Java <11.\n\
|
||||
@@ -441,12 +442,6 @@ game.crash.info=Сведения об отказе
|
||||
game.crash.reason=Анализатор сбоев
|
||||
game.crash.reason.analyzing=Анализирование...
|
||||
game.crash.reason.multiple=Обнаружено несколько причин:\n\n
|
||||
game.crash.reason.block=Игра отказала из-за блока в мире.\n\
|
||||
\n\
|
||||
Удалите этот блок с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
|
||||
\n\
|
||||
Тип блока: %1$s\n\
|
||||
Расположение блока: %2$s
|
||||
game.crash.reason.bootstrap_failed=Игра отказала из-за мода «%1$s».\n\
|
||||
\n\
|
||||
Попробуйте удалить или обновить его.
|
||||
@@ -457,12 +452,6 @@ game.crash.reason.duplicated_mod=Не удалось продолжить зап
|
||||
%2$s\n\
|
||||
\n\
|
||||
Каждый мод должен быть установлен только один, удалите дублирующий мод и попробуйте снова.
|
||||
game.crash.reason.entity=Игра отказала из-за сущность в мире.\n\
|
||||
\n\
|
||||
Удалите эту сущность с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
|
||||
\n\
|
||||
Тип сущность: %1$s\n\
|
||||
Расположение сущность: %2$s
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12 (и выше) несовместим с установленными модами. Следует понизить версию fabric до 0.11.7.
|
||||
game.crash.reason.mixin_apply_mod_failed=Игра отказала из-за невозможности применить mixin к моду «%1$s».\n\
|
||||
\n\
|
||||
@@ -701,10 +690,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Несовместимо с %s
|
||||
install.installer.install=Установить %s
|
||||
install.installer.install_offline=Установить/обновить из локального файла
|
||||
install.installer.install_offline.extension=Установщик (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Установить с интернета
|
||||
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine.
|
||||
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader и OptiFine.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Не установлено
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -724,14 +712,12 @@ install.new_game.malformed=Недопустимое имя
|
||||
install.select=Выберите операцию
|
||||
install.success=Успешно установлено.
|
||||
|
||||
java.add=Добавить Java
|
||||
java.add.failed=Этот Java недопустим или несовместим с текущей платформой.
|
||||
java.disable=Отключить Java
|
||||
java.disable.confirm=Вы уверены, что хотите отключить эту Java?
|
||||
java.disabled.management=Отключенная Java
|
||||
java.disabled.management.remove=Удалить эту Java из списка
|
||||
java.disabled.management.restore=Включить эту Java
|
||||
java.download=Скачать Java
|
||||
java.download.banshanjdk-8=Скачать Banshan JDK 8
|
||||
java.download.load_list.failed=Не удалось загрузить список версий
|
||||
java.download.more=Больше дистрибутивов Java
|
||||
@@ -1032,7 +1018,6 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Моды
|
||||
mods.add=Добавить мод
|
||||
mods.add.failed=Не удалось установить мод %s.
|
||||
mods.add.success=%s был успешно добавлен.
|
||||
mods.broken_dependency.title=Сломанная зависимость
|
||||
@@ -1051,7 +1036,6 @@ mods.check_updates.failed_download=Не удалось скачать некот
|
||||
mods.check_updates.file=Файл
|
||||
mods.check_updates.source=Источник
|
||||
mods.check_updates.target_version=Целевая версия
|
||||
mods.choose_mod=Выберите мод
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=Встроенные зависимости (Уже упакован в файл мода автором. Нет необходимости скачивать отдельно.)
|
||||
mods.dependency.optional=Необязательные зависимости (Если он отсутствует, игра будет работать нормально, но функции мода могут отсутствовать.)
|
||||
@@ -1073,7 +1057,7 @@ mods.mcmod.page=Страница MCMod
|
||||
mods.mcmod.search=Искать на MCMod
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Название
|
||||
mods.not_modded=Для управления модами необходимо сначала установить загрузчик (Forge, NeoForge, Fabric, Quilt или LiteLoader)!
|
||||
mods.not_modded=Для управления модами необходимо сначала установить загрузчик (Forge, NeoForge, Fabric, Legacy Fabric, Quilt или LiteLoader)!
|
||||
mods.restore=Восстановить
|
||||
mods.url=Официальная страница
|
||||
mods.update_modpack_mod.warning=Обновление модов в пакете модов может привести к непоправимым результатам, возможно, повредить пакет модов так, что он не сможет запуститься. Вы уверены, что хотите обновить?
|
||||
@@ -1086,9 +1070,6 @@ nbt.save.failed=Не удалось сохранить файл
|
||||
nbt.title=Смотреть файл - %s
|
||||
|
||||
datapack=Наборы данных
|
||||
datapack.add=Установить
|
||||
datapack.choose_datapack=Выберите набор данных для импорта
|
||||
datapack.extension=Набор данных
|
||||
datapack.title=Мир [%s] - Наборы данных
|
||||
|
||||
web.failed=Не удалось загрузить страницу
|
||||
@@ -1096,9 +1077,11 @@ web.open_in_browser=Хотите ли вы открыть этот адрес в
|
||||
web.view_in_browser=Смотреть в браузере
|
||||
|
||||
world=Миры
|
||||
world.add=Добавить мир
|
||||
world.add.already_exists=Мир уже существует.
|
||||
world.add.title=Выберите архив мира, который хотите импортировать
|
||||
world.add.failed=Не удалось импортировать этот мир\: %s
|
||||
world.add.invalid=Не удалось разобрать мир.
|
||||
world.backup=Резервный мир
|
||||
world.backup.create.new_one=Создать новый
|
||||
world.backup.create.failed=Не удалось создать резервную копию.\n%s
|
||||
world.backup.create.success=Успешно создано новое резервное копирование: %s
|
||||
world.backup.delete=Удалить эту резервную копию
|
||||
@@ -1115,18 +1098,12 @@ world.delete=Удалить этот мир
|
||||
world.delete.failed=Не удалось удалить мир.\n%s
|
||||
world.datapack=Наборы данных
|
||||
world.datetime=Последний запуск игры %s
|
||||
world.download=Скачать мир
|
||||
world.download.title=Скачать мир - %1s
|
||||
world.export=Экспорт мира
|
||||
world.export.title=Выберите папку для экспорта мира
|
||||
world.export.location=Экспорт в
|
||||
world.export.wizard=Экспорт мира %s
|
||||
world.extension=Архив мира
|
||||
world.game_version=Версия игры
|
||||
world.import.already_exists=Мир уже существует.
|
||||
world.import.choose=Выберите архив мира, который хотите импортировать
|
||||
world.import.failed=Не удалось импортировать этот мир\: %s
|
||||
world.import.invalid=Не удалось разобрать мир.
|
||||
world.info=Сведения о мире
|
||||
world.info.basic=Основные сведения
|
||||
world.info.allow_cheats=Разрешить команды/читы
|
||||
@@ -1192,10 +1169,8 @@ resourcepack.download.title=Скачать пакет ресурсов - %1s
|
||||
reveal.in_file_manager=Открыть в файловый менеджер
|
||||
|
||||
schematics=Схемы
|
||||
schematics.add=Добавить файлы схем
|
||||
schematics.add.failed=Не удалось добавить файлы схем
|
||||
schematics.back_to=Назад на «%s»
|
||||
schematics.create_directory=Создать папку
|
||||
schematics.create_directory.prompt=Введите новое имя папки
|
||||
schematics.create_directory.failed=Не удалось создать папку
|
||||
schematics.create_directory.failed.already_exists=Папка уже существует
|
||||
@@ -1323,9 +1298,6 @@ settings.game.java_directory.template=%s (%s)
|
||||
settings.game.management=Инструменты
|
||||
settings.game.working_directory=Рабочая папка
|
||||
settings.game.working_directory.choose=Выберите рабочую папку
|
||||
settings.game.working_directory.hint=Включите параметр «Раздельно» в разделе «Рабочая папка», чтобы позволить текущему экземпляру хранить свои настройки, сохранения и моды в отдельной папке.\n\
|
||||
\n\
|
||||
Рекомендуется включить этот параметр, чтобы избежать конфликтов с модами, но нужно будет переместить свои сохранения вручную.
|
||||
|
||||
settings.icon=Значок
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID - це унікальний іденти
|
||||
\n\
|
||||
Ця опція призначена лише для досвідчених користувачів. Ми не рекомендуємо змінювати цю опцію, якщо ви не знаєте, що робите.
|
||||
account.methods.offline.uuid.malformed=Недійсний формат.
|
||||
account.methods.forgot_password=Забули пароль
|
||||
account.methods.ban_query=Запит блокування
|
||||
account.missing=Немає облікових записів
|
||||
account.missing.add=Натисніть тут, щоб додати.
|
||||
@@ -342,7 +341,6 @@ download.provider.official=З офіційних джерел
|
||||
download.provider.balanced=З найшвидшого доступного
|
||||
download.provider.mirror=З дзеркала
|
||||
download.java=Завантаження Java
|
||||
download.java.override=Ця версія Java вже існує. Бажаєте видалити та перевстановити її?
|
||||
download.java.process=Процес завантаження Java
|
||||
download.javafx=Завантаження залежностей для лаунчера...
|
||||
download.javafx.notes=Ми зараз завантажуємо залежності для HMCL з Інтернету. \n\
|
||||
@@ -363,11 +361,14 @@ exception.ssl_handshake=Не вдалося встановити SSL-з'єдна
|
||||
exception.dns.pollution=Не вдалося встановити SSL-з'єднання. Можливо, DNS неправильно розв'язується. Спробуйте змінити DNS-сервер або використовувати проксі-сервіс.
|
||||
|
||||
extension.bat=Пакетний файл Windows
|
||||
extension.mod=Файл мода
|
||||
extension.png=Файл зображення
|
||||
extension.ps1=Скрипт PowerShell Windows
|
||||
extension.sh=Скрипт оболонки
|
||||
|
||||
extension.datapack=Datapack
|
||||
extension.mod=Файл мода
|
||||
extension.world=Архів світу
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Лаунчер не може створити каталог HMCL (%s). Перемістіть HMCL в інше місце та відкрийте його знову.
|
||||
fatal.javafx.incompatible=Відсутнє середовище JavaFX. \n\
|
||||
Hello Minecraft! Лаунчер не може автоматично встановити JavaFX на Java <11. \n\
|
||||
@@ -426,12 +427,6 @@ game.crash.info=Інформація про збій
|
||||
game.crash.reason=Причина збою
|
||||
game.crash.reason.analyzing=Аналіз...
|
||||
game.crash.reason.multiple=Виявлено кілька причин:\n\n
|
||||
game.crash.reason.block=Гра зазнала збою через блок у світі. \n\
|
||||
\n\
|
||||
Ви можете спробувати видалити цей блок за допомогою MCEdit або видалити мод, який його додав. \n\
|
||||
\n\
|
||||
Тип блоку: %1$s \n\
|
||||
Місцезнаходження: %2$s
|
||||
game.crash.reason.bootstrap_failed=Гра зазнала збою через мод "%1$s". \n\
|
||||
\n\
|
||||
Ви можете спробувати видалити або оновити його.
|
||||
@@ -440,10 +435,6 @@ game.crash.reason.debug_crash=Гра зазнала збою, оскільки
|
||||
game.crash.reason.duplicated_mod=Гра не може продовжувати роботу через дублікати модів "%1$s". \n\
|
||||
%2$s \n\
|
||||
Кожен мод можна встановити лише один раз. Видаліть дублікат мода та спробуйте знову.
|
||||
game.crash.reason.entity=Гра зазнала збою через сутність у світі. \n\
|
||||
Ви можете спробувати видалити сутність за допомогою MCEdit або видалити мод, який її додав. \n\
|
||||
Тип сутності: %1$s \n\
|
||||
Місцезнаходження: %2$s
|
||||
game.crash.reason.modmixin_failure=Гра зазнала збою, оскільки деякі моди не вдалося ввести. \n\
|
||||
Це, як правило, означає, що мод має помилку або несумісний з поточним середовищем. \n\
|
||||
Ви можете перевірити журнал, щоб знайти помилковий мод.
|
||||
@@ -650,10 +641,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Несумісний з %s
|
||||
install.installer.install=Встановлення %s
|
||||
install.installer.install_offline=Встановити/Оновити з локального файлу
|
||||
install.installer.install_offline.extension=Інсталятор (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Онлайн встановлення
|
||||
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt та LiteLoader.
|
||||
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt та LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Не встановлено
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -673,14 +663,12 @@ install.new_game.malformed=Недійсна назва.
|
||||
install.select=Виберіть операцію
|
||||
install.success=Успішно встановлено.
|
||||
|
||||
java.add=Додати Java
|
||||
java.add.failed=Ця Java недійсна або несумісна з поточною платформою.
|
||||
java.disable=Вимкнути Java
|
||||
java.disable.confirm=Ви впевнені, що хочете вимкнути цю Java?
|
||||
java.disabled.management=Вимкнена Java
|
||||
java.disabled.management.remove=Видалити цю Java зі списку
|
||||
java.disabled.management.restore=Повторно ввімкнути цю Java
|
||||
java.download=Завантажити Java
|
||||
java.download.banshanjdk-8=Завантажити Banshan JDK 8
|
||||
java.download.load_list.failed=Не вдалося завантажити список версій
|
||||
java.download.more=Більше дистрибутивів Java
|
||||
@@ -977,7 +965,6 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Моди
|
||||
mods.add=Додати мод
|
||||
mods.add.failed=Не вдалося додати мод %s.
|
||||
mods.add.success=%s успішно додано.
|
||||
mods.broken_dependency.title=Зламана залежність
|
||||
@@ -996,7 +983,6 @@ mods.check_updates.failed_download=Не вдалося завантажити д
|
||||
mods.check_updates.file=Файл
|
||||
mods.check_updates.source=Джерело
|
||||
mods.check_updates.target_version=Цільова версія
|
||||
mods.choose_mod=Вибрати мод
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=Вбудовані залежності (Вже запаковані в файл мода автором. Не потрібно завантажувати окремо)
|
||||
mods.dependency.optional=Необов'язкові залежності (Якщо відсутні, гра буде працювати нормально, але функції мода можуть бути відсутні)
|
||||
@@ -1018,7 +1004,7 @@ mods.mcmod.page=Сторінка MCMod
|
||||
mods.mcmod.search=Пошук в MCMod
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Назва
|
||||
mods.not_modded=Ви повинні спочатку встановити завантажувач модів (Forge, NeoForge, Fabric, Quilt або LiteLoader), щоб керувати своїми модами!
|
||||
mods.not_modded=Ви повинні спочатку встановити завантажувач модів (Forge, NeoForge, Fabric, Legacy Fabric, Quilt або LiteLoader), щоб керувати своїми модами!
|
||||
mods.restore=Відновити
|
||||
mods.url=Офіційна сторінка
|
||||
mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити?
|
||||
@@ -1031,9 +1017,6 @@ nbt.save.failed=Не вдалося зберегти файл
|
||||
nbt.title=Перегляд файлу - %s
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Встановити Datapack
|
||||
datapack.choose_datapack=Виберіть datapack для імпорту
|
||||
datapack.extension=Datapack
|
||||
datapack.title=Світ [%s] - Datapacks
|
||||
|
||||
web.failed=Не вдалося завантажити сторінку
|
||||
@@ -1041,9 +1024,11 @@ web.open_in_browser=Бажаєте відкрити цю адресу в бра
|
||||
web.view_in_browser=Переглянути все в браузері
|
||||
|
||||
world=Світи
|
||||
world.add=Додати світ
|
||||
world.add.already_exists=Цей світ вже існує.
|
||||
world.add.title=Виберіть архів світу, який ви хочете імпортувати
|
||||
world.add.failed=Не вдалося імпортувати цей світ: %s
|
||||
world.add.invalid=Не вдалося розібрати світ.
|
||||
world.backup=Резервне копіювання світу
|
||||
world.backup.create.new_one=Створити нову резервну копію
|
||||
world.backup.create.failed=Не вдалося створити резервну копію. \n%s
|
||||
world.backup.create.success=Успішно створено нову резервну копію: %s
|
||||
world.backup.delete=Видалити цю резервну копію
|
||||
@@ -1060,18 +1045,12 @@ world.datapack=Datapacks
|
||||
world.datetime=Останній раз грали %s
|
||||
world.delete=Видалити цей світ
|
||||
world.delete.failed=Не вдалося видалити світ.\n%s
|
||||
world.download=Завантажити світ
|
||||
world.download.title=Завантажити світ - %1s
|
||||
world.export=Експортувати світ
|
||||
world.export.title=Виберіть каталог для цього експортованого світу
|
||||
world.export.location=Зберегти як
|
||||
world.export.wizard=Експортувати світ "%s"
|
||||
world.extension=Архів світу
|
||||
world.game_version=Версія гри
|
||||
world.import.already_exists=Цей світ вже існує.
|
||||
world.import.choose=Виберіть архів світу, який ви хочете імпортувати
|
||||
world.import.failed=Не вдалося імпортувати цей світ: %s
|
||||
world.import.invalid=Не вдалося розібрати світ.
|
||||
world.info=Інформація про світ
|
||||
world.info.basic=Базова інформація
|
||||
world.info.allow_cheats=Дозволити команди/чіти
|
||||
@@ -1138,10 +1117,8 @@ resourcepack.download.title=Завантажити пакет ресурсів -
|
||||
reveal.in_file_manager=Показати в менеджері файлів
|
||||
|
||||
schematics=Схематики
|
||||
schematics.add=Додати файли схематик
|
||||
schematics.add.failed=Не вдалося додати файли схематик
|
||||
schematics.back_to=Назад до "%s"
|
||||
schematics.create_directory=Створити каталог
|
||||
schematics.create_directory.prompt=Введіть нову назву каталогу
|
||||
schematics.create_directory.failed=Не вдалося створити каталог
|
||||
schematics.create_directory.failed.already_exists=Каталог вже існує
|
||||
@@ -1268,9 +1245,6 @@ settings.game.java_directory.template=%s (%s)
|
||||
settings.game.management=Керувати
|
||||
settings.game.working_directory=Робочий каталог
|
||||
settings.game.working_directory.choose=Вибрати робочий каталог
|
||||
settings.game.working_directory.hint=Увімкніть опцію "Ізольований" у "Робочий каталог", щоб дозволити поточному екземпляру зберігати свої налаштування, збереження та моди в окремому каталозі. \n\
|
||||
\n\
|
||||
Рекомендується увімкнути цю опцію, щоб уникнути конфліктів модів, але вам доведеться перемістити свої збереження вручну.
|
||||
|
||||
settings.icon=Іконка
|
||||
|
||||
@@ -1401,7 +1375,6 @@ version.manage=Усі екземпляри
|
||||
version.manage.clean=Видалити файли журналів
|
||||
version.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports".
|
||||
version.manage.duplicate=Дублювати екземпляр
|
||||
version.manage.duplicate.duplicate_save=Дублювати збереження
|
||||
version.manage.duplicate.prompt=Введіть нову назву екземпляра
|
||||
version.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями.
|
||||
version.manage.manage=Редагувати екземпляр
|
||||
@@ -1409,7 +1382,6 @@ version.manage.manage.title=Редагувати екземпляр - %1s
|
||||
version.manage.redownload_assets_index=Оновити ресурси гри
|
||||
version.manage.remove=Видалити екземпляр
|
||||
version.manage.remove.confirm.trash=Ви впевнені, що хочете видалити екземпляр "%s"? Ви все ще можете знайти його файли у своєму кошику за назвою "%s".
|
||||
version.manage.remove.confirm.independent=Оскільки цей екземпляр зберігається в ізольованому каталозі, його видалення також видалить його збереження та інші дані. Ви все ще хочете видалити екземпляр "%s"?
|
||||
version.manage.remove_assets=Видалити всі ресурси
|
||||
version.manage.remove_libraries=Видалити всі бібліотеки
|
||||
version.manage.rename=Перейменувати екземпляр
|
||||
|
||||
@@ -117,12 +117,13 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros
|
||||
account.methods.microsoft.logging_in=登入中……
|
||||
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
|
||||
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
|
||||
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
|
||||
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
|
||||
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
|
||||
account.methods.microsoft.profile=編輯帳戶個人信息
|
||||
account.methods.microsoft.methods.device=掃描 QR Code 登入
|
||||
account.methods.microsoft.methods.device.hint=掃描 QR Code 或訪問 <a href="%s">%s</a>,在開啟的頁面中輸入 <b>%s</b> 完成登入。
|
||||
account.methods.microsoft.methods.browser=在瀏覽器中登入
|
||||
account.methods.microsoft.methods.browser.hint=點擊「登入」按鈕或者<a href="%s">複製連結</a>並在瀏覽器中貼上以登入。
|
||||
account.methods.microsoft.manual=<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>
|
||||
account.methods.microsoft.profile=編輯帳戶個人資訊
|
||||
account.methods.microsoft.purchase=購買 Minecraft
|
||||
account.methods.forgot_password=忘記密碼
|
||||
account.methods.ban_query=查詢帳戶是否被封禁
|
||||
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 進行登入。
|
||||
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。請下載官方版本來重新整理帳戶。
|
||||
@@ -362,7 +363,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
|
||||
download.provider.mirror=盡量使用鏡像源
|
||||
download.provider.mirror.desc=載入快,但可能不是最新
|
||||
download.java=下載 Java
|
||||
download.java.override=此 Java 版本已經存在,是否移除並重新安裝?
|
||||
download.java.process=下載 Java
|
||||
download.javafx=正在下載必要的執行時元件
|
||||
download.javafx.notes=正在透過網路下載 HMCL 必要的執行時元件。\n點擊「切換下載源」按鈕查看詳情以及選取下載源。點擊「取消」按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切換下載源。
|
||||
@@ -380,12 +380,18 @@ exception.ssl_handshake=無法建立 SSL 連線。目前 Java 缺少相關的 SS
|
||||
exception.dns.pollution=無法建立 SSL 連線。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。
|
||||
|
||||
extension.bat=Windows 批次檔
|
||||
extension.mod=模組檔案
|
||||
extension.png=圖片檔案
|
||||
extension.ps1=PowerShell 指令碼
|
||||
extension.sh=Bash 指令碼
|
||||
extension.command=macOS Shell 指令碼
|
||||
|
||||
extension.datapack=資料包壓縮檔
|
||||
extension.mod=模組檔案
|
||||
extension.modloader.installer=模組載入器安裝檔
|
||||
extension.resourcepack=資源包壓縮檔
|
||||
extension.schematic=原理圖檔案
|
||||
extension.world=世界壓縮檔
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
|
||||
fatal.javafx.incompatible=缺少 JavaFX 執行環境。\nHMCL 無法在低於 Java 11 的 Java 環境上自行補全 JavaFX 執行環境。請更新到 Java 11 或更高版本。
|
||||
fatal.javafx.incomplete=JavaFX 執行環境不完整。請嘗試更換你的 Java 或者重新安裝 OpenJFX。
|
||||
@@ -394,7 +400,7 @@ fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft
|
||||
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
|
||||
fatal.config_unsupported_version=目前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被保存。
|
||||
fatal.config_unsupported_version=目前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被儲存。
|
||||
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
@@ -429,13 +435,13 @@ game.crash.info=遊戲訊息
|
||||
game.crash.reason=崩潰原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.multiple=檢測到多個原因:\n\n
|
||||
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s
|
||||
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
|
||||
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
|
||||
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
|
||||
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s
|
||||
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
|
||||
@@ -533,10 +539,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=與 %s 不相容
|
||||
install.installer.install=安裝 %s
|
||||
install.installer.install_offline=從本機檔案安裝或升級
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安裝器
|
||||
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge、Cleanroom 及 OptiFine 安裝器
|
||||
install.installer.install_online=線上安裝
|
||||
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=未安裝
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -556,7 +561,7 @@ install.new_game.malformed=名稱無效
|
||||
install.select=請選取安裝方式
|
||||
install.success=安裝成功
|
||||
|
||||
java.add=添加 Java
|
||||
java.add=新增 Java
|
||||
java.add.failed=Java 無效或與目前平臺不相容
|
||||
java.disable=停用此 Java
|
||||
java.disable.confirm=你確定要停用此 Java 嗎?
|
||||
@@ -567,6 +572,7 @@ java.download=下載 Java
|
||||
java.download.banshanjdk-8=下載 Banshan JDK 8
|
||||
java.download.load_list.failed=載入版本清單失敗
|
||||
java.download.more=更多發行版
|
||||
java.download.title=下載 Java
|
||||
java.download.prompt=請選擇你要下載的 Java 版本:
|
||||
java.download.distribution=發行版
|
||||
java.download.version=版本
|
||||
@@ -855,6 +861,7 @@ mods=模組
|
||||
mods.add=新增模組
|
||||
mods.add.failed=新增模組「%s」失敗。
|
||||
mods.add.success=成功新增模組「%s」。
|
||||
mods.add.title=選取要新增的模組檔案
|
||||
mods.broken_dependency.title=損壞的相依模組
|
||||
mods.broken_dependency.desc=該相依模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源。
|
||||
mods.category=類別
|
||||
@@ -871,7 +878,6 @@ mods.check_updates.failed_download=部分檔案下載失敗
|
||||
mods.check_updates.file=檔案
|
||||
mods.check_updates.source=來源
|
||||
mods.check_updates.target_version=目標版本
|
||||
mods.choose_mod=選取模組
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=內建相依模組 (作者已經打包在模組檔中,無需單獨下載)
|
||||
mods.dependency.optional=可選相依模組 (如果不安裝,遊戲可以執行,但模組功能可能缺失)
|
||||
@@ -893,7 +899,7 @@ mods.mcmod.page=MC 百科頁面
|
||||
mods.mcmod.search=MC 百科檢索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=名稱
|
||||
mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge、Fabric、Quilt 或 LiteLoader 才能管理模組。
|
||||
mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge、Fabric、Legacy Fabric、Quilt 或 LiteLoader 才能管理模組。
|
||||
mods.restore=回退
|
||||
mods.url=官方頁面
|
||||
mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
|
||||
@@ -917,8 +923,7 @@ nbt.title=查看檔案 - %s
|
||||
|
||||
datapack=資料包
|
||||
datapack.add=新增資料包
|
||||
datapack.choose_datapack=選取要匯入的資料包壓縮檔
|
||||
datapack.extension=資料包
|
||||
datapack.add.title=選取要新增的資料包壓縮檔
|
||||
datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包
|
||||
datapack.title=世界 [%s] - 資料包
|
||||
|
||||
@@ -928,6 +933,10 @@ web.view_in_browser=在瀏覽器中查看完整日誌
|
||||
|
||||
world=世界
|
||||
world.add=新增世界
|
||||
world.add.already_exists=此世界已經存在
|
||||
world.add.failed=無法新增此世界: %s
|
||||
world.add.invalid=無法識別的世界壓縮檔
|
||||
world.add.title=選取要新增的世界壓縮檔
|
||||
world.backup=世界備份
|
||||
world.backup.create.new_one=建立新備份
|
||||
world.backup.create.failed=建立備份失敗。\n%s
|
||||
@@ -956,7 +965,6 @@ world.export=匯出此世界
|
||||
world.export.title=選取該世界的儲存位置
|
||||
world.export.location=儲存到
|
||||
world.export.wizard=匯出世界「%s」
|
||||
world.extension=世界壓縮檔
|
||||
world.icon=世界圖示
|
||||
world.icon.change=修改世界圖示
|
||||
world.icon.change.fail.load.title=圖片解析失敗
|
||||
@@ -966,10 +974,6 @@ world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不
|
||||
world.icon.change.succeed.toast=世界圖示修改成功
|
||||
world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被 Minecraft 解析。
|
||||
world.icon.choose.title=選擇世界圖示
|
||||
world.import.already_exists=此世界已經存在
|
||||
world.import.choose=選取要匯入的世界壓縮檔
|
||||
world.import.failed=無法匯入此世界: %s
|
||||
world.import.invalid=無法識別的世界壓縮檔
|
||||
world.info=世界資訊
|
||||
world.info.basic=基本資訊
|
||||
world.info.allow_cheats=允許指令(作弊)
|
||||
@@ -1035,6 +1039,7 @@ repositories.chooser.title=選取 JavaFX 下載源
|
||||
|
||||
resourcepack=資源包
|
||||
resourcepack.add=新增資源包
|
||||
resourcepack.add.title=選取要新增的資源包壓縮檔
|
||||
resourcepack.manage=資源包管理
|
||||
resourcepack.download=下載資源包
|
||||
resourcepack.add.failed=新增資源包失敗
|
||||
@@ -1044,8 +1049,9 @@ resourcepack.download.title=資源包下載 - %1s
|
||||
reveal.in_file_manager=在檔案管理員中查看
|
||||
|
||||
schematics=原理圖
|
||||
schematics.add=添加原理圖
|
||||
schematics.add.failed=添加原理圖失敗
|
||||
schematics.add=新增原理圖
|
||||
schematics.add.failed=新增原理圖失敗
|
||||
schematics.add.title=選取要新增的原理圖檔案
|
||||
schematics.back_to=返回到「%s」
|
||||
schematics.create_directory=建立目錄
|
||||
schematics.create_directory.prompt=請輸入新目錄名稱
|
||||
@@ -1267,7 +1273,7 @@ terracotta.back=退出
|
||||
terracotta.feedback.title=填寫回饋表
|
||||
terracotta.feedback.desc=在 HMCL 更新聯機核心時,我們歡迎您用 10 秒時間填寫聯機品質回饋收集表。
|
||||
terracotta.sudo_installing=HMCL 需要驗證您的密碼才能安裝線上核心
|
||||
terracotta.difficulty.easiest=當前網路狀況極佳:稍等一下就成功!
|
||||
terracotta.difficulty.easiest=目前網路狀況極佳:稍等一下就成功!
|
||||
terracotta.difficulty.simple=目前網路狀況較好:建立連線需要一段時間…
|
||||
terracotta.difficulty.medium=目前網路狀態中等:已啟用抗干擾備用線路,連線可能失敗
|
||||
terracotta.difficulty.tough=目前網路狀態極差:已啟用抗干擾備用線路,連線可能失敗
|
||||
@@ -1277,7 +1283,7 @@ terracotta.from_local.desc=在部分地區,內建的預設下載管道可能
|
||||
terracotta.from_local.guide=您應下載名為 %s 的線上核心套件。下載完成後,請將檔案拖曳到目前介面來安裝。
|
||||
terracotta.from_local.file_name_mismatch=您應該下載名為 %1$s 的線上核心包,而非 %2$s
|
||||
terracotta.export_log=匯出線上核心日誌
|
||||
terracotta.export_log.desc=為分析錯誤提供更多信息
|
||||
terracotta.export_log.desc=為分析錯誤提供更多資訊
|
||||
terracotta.status.bootstrap=正在收集資訊
|
||||
terracotta.status.uninitialized.not_exist=未下載聯機核心
|
||||
terracotta.status.uninitialized.not_exist.title=下載聯機核心 (約 8MiB)
|
||||
@@ -1285,8 +1291,8 @@ terracotta.status.uninitialized.update=需更新聯機核心
|
||||
terracotta.status.uninitialized.update.title=更新聯機核心 (約 8MiB)
|
||||
terracotta.status.uninitialized.desc=您承諾,在多人聯機全過程中,您將嚴格遵守您所在國家或地區的全部法律法規
|
||||
terracotta.confirm.title=使用者須知
|
||||
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請通過相關管道進行回饋。\n\
|
||||
陶瓦聯機使用 P2P 技術,聯機成功後房間內用戶之間將直接連接,不會使用第三方伺服器對您的流量進行轉發。最終聯機體驗和參與聯機者的網路情況有較大關係。\n\
|
||||
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請透過相關管道進行回饋。\n\
|
||||
陶瓦聯機使用 P2P 技術,連線成功後房間內使用者之間將直接連接,不會使用第三方伺服器對您的流量進行轉發。最終聯機體驗和參與連線者的網路情況有較大關係。\n\
|
||||
在多人聯機全過程中,您必須嚴格遵守您所在國家與地區的全部法律法規。
|
||||
terracotta.status.preparing=正在下載聯機核心 (請勿退出啟動器)
|
||||
terracotta.status.launching=正在初始化聯機核心
|
||||
|
||||
@@ -118,13 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软
|
||||
account.methods.microsoft.logging_in=登录中……
|
||||
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
|
||||
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
|
||||
account.methods.microsoft.manual=请在弹出的网页中输入上方显示的代码以完成登录。\n\
|
||||
若网站未能显示,请手动在浏览器中打开:%s\n\
|
||||
<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
|
||||
account.methods.microsoft.methods.device=扫描二维码登录
|
||||
account.methods.microsoft.methods.device.hint=扫描二维码或访问 <a href="%s">%s</a>,在打开的页面中输入 <b>%s</b> 完成登录。
|
||||
account.methods.microsoft.methods.browser=在浏览器登录
|
||||
account.methods.microsoft.methods.browser.hint=点击“登录”按钮或者<a href="%s">复制链接</a>并在浏览器中粘贴以登录。
|
||||
account.methods.microsoft.manual=<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
|
||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.profile=编辑账户个人信息
|
||||
account.methods.microsoft.purchase=购买 Minecraft
|
||||
account.methods.forgot_password=忘记密码
|
||||
account.methods.ban_query=检测账户是否被封禁
|
||||
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 来登录微软账户。
|
||||
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。请下载官方版本来刷新账户。
|
||||
@@ -364,7 +365,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
|
||||
download.provider.mirror=尽量使用镜像源
|
||||
download.provider.mirror.desc=加载快,但可能不是最新
|
||||
download.java=下载 Java
|
||||
download.java.override=此 Java 版本已经存在,是否卸载并重新安装?
|
||||
download.java.process=下载 Java
|
||||
download.javafx=正在下载必要的运行时组件……
|
||||
download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源。点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源。
|
||||
@@ -383,12 +383,18 @@ exception.ssl_handshake=无法建立 SSL 连接。当前 Java 缺少相关的 SS
|
||||
exception.dns.pollution=无法建立 SSL 连接。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。
|
||||
|
||||
extension.bat=Windows 脚本
|
||||
extension.mod=模组文件
|
||||
extension.png=图片文件
|
||||
extension.ps1=PowerShell 脚本
|
||||
extension.sh=Bash 脚本
|
||||
extension.command=macOS Shell 脚本
|
||||
|
||||
extension.datapack=数据包压缩包
|
||||
extension.mod=模组文件
|
||||
extension.modloader.installer=模组加载器安装包
|
||||
extension.resourcepack=资源包压缩包
|
||||
extension.schematic=原理图文件
|
||||
extension.world=世界压缩包
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHello Minecraft! Launcher 无法在低于 Java 11 的 Java 环境上自行补全 JavaFX 运行环境。请更新到 Java 11 或更高版本。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.javafx.incomplete=JavaFX 运行环境不完整。请尝试更换你的 Java 或者重新安装 OpenJFX。
|
||||
@@ -432,14 +438,14 @@ game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如
|
||||
game.crash.info=游戏信息
|
||||
game.crash.reason=崩溃原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s
|
||||
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
|
||||
game.crash.reason.multiple=检测到多个原因:\n\n
|
||||
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
|
||||
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
|
||||
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s
|
||||
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
|
||||
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
|
||||
@@ -538,10 +544,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=与 %s 不兼容
|
||||
install.installer.install=安装 %s
|
||||
install.installer.install_offline=从本地文件安装/升级
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安装器
|
||||
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge、Cleanroom 和 OptiFine 安装器
|
||||
install.installer.install_online=在线安装
|
||||
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=未安装
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -572,6 +577,7 @@ java.download=下载 Java
|
||||
java.download.banshanjdk-8=下载 Banshan JDK 8
|
||||
java.download.load_list.failed=加载版本列表失败
|
||||
java.download.more=更多发行版
|
||||
java.download.title=下载 Java
|
||||
java.download.prompt=请选择你要下载的 Java 版本:
|
||||
java.download.distribution=发行版
|
||||
java.download.version=版本
|
||||
@@ -860,6 +866,7 @@ mods=模组
|
||||
mods.add=添加模组
|
||||
mods.add.failed=添加模组“%s”失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
mods.add.success=成功添加模组 %s。
|
||||
mods.add.title=选择要添加的模组文件
|
||||
mods.broken_dependency.title=损坏的前置模组
|
||||
mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。
|
||||
mods.category=类别
|
||||
@@ -876,7 +883,6 @@ mods.check_updates.failed_download=部分文件下载失败
|
||||
mods.check_updates.file=文件
|
||||
mods.check_updates.source=来源
|
||||
mods.check_updates.target_version=目标版本
|
||||
mods.choose_mod=选择模组
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=内置的前置模组 (已经由作者打包在模组文件中,无需另外下载)
|
||||
mods.dependency.optional=可选的前置模组 (若缺失游戏能够正常运行,但模组功能可能缺失)
|
||||
@@ -898,7 +904,7 @@ mods.mcmod.page=MC 百科页面
|
||||
mods.mcmod.search=MC 百科搜索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=名称
|
||||
mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge、Fabric、Quilt 或 LiteLoader 才能管理模组。
|
||||
mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge、Fabric、Legacy Fabric、Quilt 或 LiteLoader 才能管理模组。
|
||||
mods.restore=回退
|
||||
mods.url=官方页面
|
||||
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
|
||||
@@ -922,8 +928,7 @@ nbt.title=查看文件 - %s
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
datapack.choose_datapack=选择要导入的数据包压缩包
|
||||
datapack.extension=数据包
|
||||
datapack.add.title=选择要添加的数据包压缩包
|
||||
datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包
|
||||
datapack.title=世界 [%s] - 数据包
|
||||
|
||||
@@ -933,6 +938,10 @@ web.view_in_browser=在浏览器中查看完整日志
|
||||
|
||||
world=世界
|
||||
world.add=添加世界
|
||||
world.add.already_exists=此世界已经存在
|
||||
world.add.failed=无法添加此世界:%s
|
||||
world.add.invalid=无法识别该世界压缩包
|
||||
world.add.title=选择要添加的世界压缩包
|
||||
world.backup=备份管理
|
||||
world.backup.create.new_one=创建新备份
|
||||
world.backup.create.failed=创建备份失败。\n%s
|
||||
@@ -961,7 +970,6 @@ world.export=导出此世界
|
||||
world.export.title=选择该世界的存储位置
|
||||
world.export.location=保存到
|
||||
world.export.wizard=导出世界“%s”
|
||||
world.extension=世界压缩包
|
||||
world.game_version=游戏版本
|
||||
world.icon=世界图标
|
||||
world.icon.change=修改世界图标
|
||||
@@ -972,10 +980,6 @@ world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不
|
||||
world.icon.change.succeed.toast=世界图标修改成功
|
||||
world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被 Minecraft 解析。
|
||||
world.icon.choose.title=选择世界图标
|
||||
world.import.already_exists=此世界已经存在
|
||||
world.import.choose=选择要导入的世界压缩包
|
||||
world.import.failed=无法导入此世界:%s
|
||||
world.import.invalid=无法识别该世界压缩包
|
||||
world.info=世界信息
|
||||
world.info.basic=基本信息
|
||||
world.info.allow_cheats=允许命令(作弊)
|
||||
@@ -1040,6 +1044,7 @@ repositories.chooser.title=选择 JavaFX 下载源
|
||||
|
||||
resourcepack=资源包
|
||||
resourcepack.add=添加资源包
|
||||
resourcepack.add.title=选择要添加的资源包压缩包
|
||||
resourcepack.manage=资源包管理
|
||||
resourcepack.download=下载资源包
|
||||
resourcepack.add.failed=添加资源包失败
|
||||
@@ -1051,6 +1056,7 @@ reveal.in_file_manager=在文件管理器中查看
|
||||
schematics=原理图
|
||||
schematics.add=添加原理图
|
||||
schematics.add.failed=添加原理图失败
|
||||
schematics.add.title=选择要添加的原理图文件
|
||||
schematics.back_to=返回到“%s”
|
||||
schematics.create_directory=创建文件夹
|
||||
schematics.create_directory.prompt=请输入新文件夹名称
|
||||
|
||||
@@ -19,20 +19,135 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
<html lang="%lang%">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello Minecraft! Launcher</title>
|
||||
<style>
|
||||
%style%
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--monet-surface);
|
||||
color: var(--monet-on-surface);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
background-color: var(--monet-surface-container-high);
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
|
||||
max-width: 560px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.icon-column {
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.icon-column svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block;
|
||||
fill: var(--monet-on-surface);
|
||||
}
|
||||
|
||||
.content-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
color: var(--monet-on-surface);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--monet-on-surface-variant);
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: transparent;
|
||||
color: var(--monet-primary);
|
||||
border: none;
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
outline: none;
|
||||
min-width: 64px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background-color: rgba(128, 128, 128, 0.08);
|
||||
}
|
||||
|
||||
.btn-confirm:active {
|
||||
background-color: rgba(128, 128, 128, 0.12);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
%close-page%
|
||||
<div class="dialog-card">
|
||||
<div class="icon-column">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
open("about:blank","_self").close();
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
<div class="content-column">
|
||||
<div class="title">%success%</div>
|
||||
<div class="subtitle">%close_page%</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn-confirm" onclick="closeWindow()">%ok%</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.close();
|
||||
window.open("about:blank", "_self").close();
|
||||
}
|
||||
|
||||
setTimeout(closeWindow, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,63 +1,63 @@
|
||||
{
|
||||
"__comment__": "THIS FILE IS MACHINE GENERATED! DO NOT EDIT!",
|
||||
"version_latest": "0.4.1",
|
||||
"version_latest": "0.4.2",
|
||||
"packages": {
|
||||
"windows-x86_64": {
|
||||
"hash": "4693fec29bb54a0bb1a1a8a263a935f1673b8f76e84d125974cf259f1912a195beab4dfd04c23cae592cf71b116e82ecd48828b1445ab75c980c8cd79c777d21",
|
||||
"hash": "6a98f524d4f00373696517306af8aa50d01d55ce4eadb27e9e4bc2f882707a0b5f20d5d4c33371d1459dcf5bf144ffed9beb414202d9ccf32b11dbbfcf19d650",
|
||||
"files": {
|
||||
"VCRUNTIME140.DLL": "3d4b24061f72c0e957c7b04a0c4098c94c8f1afb4a7e159850b9939c7210d73398be6f27b5ab85073b4e8c999816e7804fef0f6115c39cd061f4aaeb4dcda8cf",
|
||||
"terracotta-0.4.1-windows-x86_64.exe": "3d29f7deb61b8b87e14032baecad71042340f6259d7ff2822336129714470e1a951cd96ee5d6a8c9312e7cf1bb9bb9d1cbf757cfa44d2b2f214362c32ea03b5b"
|
||||
"terracotta-0.4.2-windows-x86_64.exe": "6e98d1f2380ed22fb5a2dd4aafce6c773e9cf69100c8bb8e49e7d6983756bdb9a31f80e06bcfbe5a2742144fe806d3d687dec54d8f09d87c659341f99dd9fd80"
|
||||
}
|
||||
},
|
||||
"windows-arm64": {
|
||||
"hash": "6c68da53142cc92598a9d3b803f110c77927ee7b481e2f623dfab17cd97fee4a58378e796030529713d3e394355135cc01d5f5d86cef7dbd31bbf8e913993d4c",
|
||||
"hash": "fc1077247014ac0c712469498bde2ef7f6d881d5fcb7bdd5e11ebe20218fed365be19afdb8d453a79d77b729f866058522b910741767f4df947faa891434b463",
|
||||
"files": {
|
||||
"VCRUNTIME140.DLL": "5cb5ce114614101d260f4754c09e8a0dd57e4da885ebb96b91e274326f3e1dd95ed0ade9f542f1922fad0ed025e88a1f368e791e1d01fae69718f0ec3c7b98c8",
|
||||
"terracotta-0.4.1-windows-arm64.exe": "d2cf0f29aac752d322e594da6144bbe2874aabde52b5890a6f04a33b77a377464cbf3367e40de6b046c7226517f48e23fd40e611198fcaa1f30503c36d99b20c"
|
||||
"terracotta-0.4.2-windows-arm64.exe": "30a15c5c53e5817c5a3634532172559327474741d3b2c7ef4e8a30acc6f59cdcf3570bf5f583e3cbe9e2abc8253e977c1abda1e9f36c88c4e99240da257347d0"
|
||||
}
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"hash": "c247ab9023cf47231f3a056ddf70fe823e504c19ce241bf264c0a3cf2981c044b236dc239f430afb2541445d1b61d18898f8af5e1c063f31fb952bdfbea4aff5",
|
||||
"hash": "a762e4b2d6f84e899292b9e3856d009411a516d3c47f54575f843ce082f63dff2baa68ba0faa844b8b64fb12e91017386f15f5e7f975f8ee605bf8d4217cb091",
|
||||
"files": {
|
||||
"terracotta-0.4.1-macos-x86_64": "dd2cde70a950849498425b8b00c17fb80edb8dd9bc0312da4885d97a28a4959d87782bd2202ef7f71bdbf2a92609089585c9e3faf24c10df6150e221e111a764",
|
||||
"terracotta-0.4.1-macos-x86_64.pkg": "6057d5b4ea93da45796277a20ddaea7850e8c66326ded769f20fff74e132b453d6d615d937fc8f1d62b0b36b26961df5afb76535aecf85f25d2160889225ac6d"
|
||||
"terracotta-0.4.2-macos-x86_64": "24efb85390eff88a538ed7e503fb1488e5e622730ca30c741a0e8b4c8f4e8d4868a2f9f38da8de540aeb535af2fd1e41c7081dae9c700e8a1a03b6c540218164",
|
||||
"terracotta-0.4.2-macos-x86_64.pkg": "0f80437061231018ec0f860f875aba02cae9e0b36d21f2db8e99d25948806e1119f7844a4d4acca01d6124d7079718762cdebe3756b005a55632de7029fe0d24"
|
||||
}
|
||||
},
|
||||
"macos-arm64": {
|
||||
"hash": "0ddf48a44ea2563c37707caea8381ad3c69da73dd443d07dd98fe630f4e24ccda6f1fcdc9207a66bf61758b887a7b47186883ccd12bcb6b68387d8d756873f44",
|
||||
"hash": "09e444fea2d9fd19f3e5cb62e29055228345be163924cbd408d947646fafed1012cf48508ee6a155ede3d571e2ffaa72d09ceeb1493c8a60feb05e0699f19ba3",
|
||||
"files": {
|
||||
"terracotta-0.4.1-macos-arm64": "ddc335ee082b4c0323314d482933810fc2d377cfc131a9aa978b54f245f1bed8973139edf7cf94f7ae6c04660dfe18d275b1a81638781e482b4ff585f351eed9",
|
||||
"terracotta-0.4.1-macos-arm64.pkg": "5cbb41a450f080234b66fa264351bd594e3f6ef1599c96f277baa308e34dd26caefa3a34b3d65e972bc20868f2d4a661e8b3910d4b0311a374c6ac680bdccf8f"
|
||||
"terracotta-0.4.2-macos-arm64": "8e59a9d78acd57702dc044d6f2799c6af586b075a262f7d4dbbf0876e1af8d8271e04783c24ff820b801e3b14cd0190ab8403d097f3f2d98b6d911f95ed1e972",
|
||||
"terracotta-0.4.2-macos-arm64.pkg": "b0b72f8883767c359f0700e958bd859dd5932c4674fbdf2a32f7de189fb9f54822d4825a754dc856416c8760338eb8ffca80b40c0a57c608fbe6ae9928888993"
|
||||
}
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"hash": "c00b0622203c1610a8e72bde5230fca6fe56cf1f45dc9bc7e857350f3a050d442c814084758090799b9a5b875730fa666539ee75bec706c05d9338ea208301eb",
|
||||
"hash": "d326ad95815d04568d485b5038e40ffc47ca54292fa0925eee6f5cea014024f901d661708aac2a743037b990882ad82b4d0b7bb03dc3b2fe720dbf0f3efe1c98",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-x86_64": "e53a9d8ec085ef7a7816b3da374e5c88fced2cf8319d447be3226422d01f9d7ee2019e403eafe21082135050652b2788b7d9520cc432c8d08931930b99595ed7"
|
||||
"terracotta-0.4.2-linux-x86_64": "fac328ba8957a711b03557bb913940f22d61b76608cd203fdf51024b6f94b19f5bc91c9b8a9fa80baf6968e1e6873c1880fd4cf54a2f8e3c6cf1e6ac161f8d0c"
|
||||
}
|
||||
},
|
||||
"linux-arm64": {
|
||||
"hash": "4d661e1123ca050980960fe2d32b8d93a2c3ba807748569a45f16050fb1b52089bfc64c2dd64aba652dfed0e4ac3fba1ff9518cc3f95598c99fc71d93552b589",
|
||||
"hash": "57c08f48d9535e93ad547d2dfc852d267992cc164a7208b42a2da0a6cbc2f21862f610e02a746b4b67150f4dec26b86a4f96eb9bd2f58d124d5b40ba50c6d55e",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-arm64": "d4ccf6ff8c2fac060fecaa3c8368677f9342e283f2435493b3e259e922ee0bb0b08f15a292bf91898800a462336c64d0dee7b73f03c1752e5b0988a05adb3b52"
|
||||
"terracotta-0.4.2-linux-arm64": "d807744c2041c98686e4b505324713badea7a0f31e8810be49ae053a63fb6dfc474ac58d678fb93eea0dd5cccff7372d9ec6135a1046f4b306cad35cd90ecacd"
|
||||
}
|
||||
},
|
||||
"linux-loongarch64": {
|
||||
"hash": "8297bb37617e9e7ce282fc553c5b14c84a900bcff4d025be31fd4a4da8b3943d040afc6143aa17de9a88e5fa29af7254d38db8ae6418ee539c2301632448da09",
|
||||
"hash": "a19f4ad4fa1fd2f3c1c867f8743178ab0524d1634d63e122ffc080623e4f14207ecab58830fa0d888696ef28ce4cfa189e6ffbe92c6e1aa1836652617a96a801",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-loongarch64": "ffdf7582d095307b91ddfc3e5d0daa78d434e4e34916d0cdf1520ae74b188fe5a48307047bf2da9a526eb725fe80cf230a93001bc8199d236b9cf28a1beaa6e9"
|
||||
"terracotta-0.4.2-linux-loongarch64": "7d4896b275207def0861e544cf2afc9501c1946f1bc8bb2bad9ef4510dd915eabb63eeedff725e69565cdf5c27c146b52dd75d24115d5141909c0865f89cd7b9"
|
||||
}
|
||||
},
|
||||
"linux-riscv64": {
|
||||
"hash": "092f863885205107525e7ccb0e18977f6fd3018910ca5819772ec741dd8cffee52cc352a44b928bd2ba99ab881adaadff9e3bf4bf283f7384a35fea14becb0b4",
|
||||
"hash": "ef3dfcf3c64dfcaf94adc518f0184d017a32a924f67ef33ffb8128f20669cf177cd345df3be8c00225a275bd44f70657b94fb43c60afeb21238000ac7a7f1721",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-riscv64": "a6e5d70ddc433bf804764b69e8c204a6a428ece22b9d8ab713ed339fb81bfa1d29daeb6bdfd62c85ff193396315f96172f4a28925e5a4efc45f7d6fa868782a9"
|
||||
"terracotta-0.4.2-linux-riscv64": "afddc55f16f5cf3103548d13f7a3238964c8899dba8eb0e1fdc9e354da65e91fa4486de4b8680a7a3d9326eacf18c1ddb6b5b1f9459908f35f77b36f769172ea"
|
||||
}
|
||||
},
|
||||
"freebsd-x86_64": {
|
||||
"hash": "288bd7a97b2e2c5fb3c7d8107ed1311bc881fb74cf92da40b6c84156239d0832d2306b74b1e93a683188a5db896a6506326c6a7a4ac0ab2e798fa4f1f00787f0",
|
||||
"hash": "a4d02ebbb1419f4e9265f5b24060432799151a27e0d1871690d2f3a51f40bcfd6d92c176001a15a62caa9bf31ea1b67a7b2da10e3351bfcdd36bfac1377d08d9",
|
||||
"files": {
|
||||
"terracotta-0.4.1-freebsd-x86_64": "c90e7db3c5e5cc8d53d8d10a8bf88899e2c418758c14c3d14bc24efc32a711f76882bb5b5a49db5b0256ead4f22b666139bd2a212bf09036d7e7e64ddec4ec3c"
|
||||
"terracotta-0.4.2-freebsd-x86_64": "a50da02752cef73d21804b26eb02261568ed3053c8fef1074014d50226843810f71e6eda39a53af3bad8e173cec40826631a4c8877af460d14a307f1b0a62519"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.geometry.Bounds;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/// @author Glavo
|
||||
public final class SVGTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(SVG.class)
|
||||
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
|
||||
public void testViewWindow(SVG svg) {
|
||||
SVGPath path = new SVGPath();
|
||||
path.setContent(svg.getPath());
|
||||
|
||||
Bounds layoutBounds = path.getLayoutBounds();
|
||||
assertEquals(
|
||||
new ViewBox(0, 0, 24, 24),
|
||||
new ViewBox(layoutBounds.getMinX(), layoutBounds.getMinY(), layoutBounds.getWidth(), layoutBounds.getHeight())
|
||||
);
|
||||
}
|
||||
|
||||
private record ViewBox(double minX, double minY, double width, double height) {
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,16 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class OAuth {
|
||||
public static final OAuth MICROSOFT = new OAuth(
|
||||
@@ -77,17 +81,31 @@ public class OAuth {
|
||||
|
||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||
Session session = options.callback.startServer();
|
||||
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
||||
pair("prompt", "select_account"))));
|
||||
|
||||
String codeVerifier = session.getCodeVerifier();
|
||||
String state = session.getState();
|
||||
String codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()),
|
||||
pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope),
|
||||
pair("prompt", "select_account"),
|
||||
pair("code_challenge", codeChallenge),
|
||||
pair("state", state),
|
||||
pair("code_challenge_method", "S256")
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
|
||||
.form(pair("client_id", options.callback.getClientId()),
|
||||
pair("code", code),
|
||||
pair("grant_type", "authorization_code"),
|
||||
pair("code_verifier", codeVerifier),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.retry(5)
|
||||
.getJson(AuthorizationResponse.class);
|
||||
@@ -106,7 +124,7 @@ public class OAuth {
|
||||
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
|
||||
|
||||
// Microsoft OAuth Flow
|
||||
options.callback.openBrowser(deviceTokenResponse.verificationURI);
|
||||
options.callback.openBrowser(GrantFlow.DEVICE, deviceTokenResponse.verificationURI);
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
long interval = TimeUnit.MILLISECONDS.convert(deviceTokenResponse.interval, TimeUnit.SECONDS);
|
||||
@@ -153,10 +171,6 @@ public class OAuth {
|
||||
pair("grant_type", "refresh_token")
|
||||
);
|
||||
|
||||
if (!options.callback.isPublicClient()) {
|
||||
query.put("client_secret", options.callback.getClientSecret());
|
||||
}
|
||||
|
||||
RefreshResponse response = HttpRequest.POST(tokenURL)
|
||||
.form(query)
|
||||
.accept("application/json")
|
||||
@@ -174,6 +188,20 @@ public class OAuth {
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateCodeChallenge(String codeVerifier) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
|
||||
try {
|
||||
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(bytes, 0, bytes.length);
|
||||
byte[] digest = messageDigest.digest();
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to generate code challenge", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
@@ -207,6 +235,9 @@ public class OAuth {
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
String getState();
|
||||
|
||||
String getCodeVerifier();
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
@@ -237,15 +268,12 @@ public class OAuth {
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
* @param grantFlow the grant flow.
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
|
||||
boolean isPublicClient();
|
||||
}
|
||||
|
||||
public enum GrantFlow {
|
||||
|
||||
@@ -47,10 +47,10 @@ public final class MicrosoftAccount extends OAuthAccount {
|
||||
this.characterUUID = requireNonNull(session.getProfile().getId());
|
||||
}
|
||||
|
||||
protected MicrosoftAccount(MicrosoftService service, CharacterSelector characterSelector) throws AuthenticationException {
|
||||
protected MicrosoftAccount(MicrosoftService service, OAuth.GrantFlow flow) throws AuthenticationException {
|
||||
this.service = requireNonNull(service);
|
||||
|
||||
MicrosoftSession acquiredSession = service.authenticate();
|
||||
MicrosoftSession acquiredSession = service.authenticate(flow);
|
||||
if (acquiredSession.getProfile() == null) {
|
||||
session = service.refresh(acquiredSession);
|
||||
} else {
|
||||
@@ -105,7 +105,7 @@ public final class MicrosoftAccount extends OAuthAccount {
|
||||
|
||||
@Override
|
||||
public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException {
|
||||
MicrosoftSession acquiredSession = service.authenticate();
|
||||
MicrosoftSession acquiredSession = service.authenticate(OAuth.GrantFlow.DEVICE);
|
||||
if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) {
|
||||
throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId());
|
||||
}
|
||||
|
||||
+2
-3
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth.microsoft;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -39,9 +40,7 @@ public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
|
||||
|
||||
@Override
|
||||
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
|
||||
Objects.requireNonNull(selector);
|
||||
|
||||
return new MicrosoftAccount(service, selector);
|
||||
return new MicrosoftAccount(service, (OAuth.GrantFlow) additionalData);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user