Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -18,21 +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 {
|
||||
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"),
|
||||
@@ -50,9 +46,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 +88,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 +99,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 +115,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 +130,49 @@ public enum SVG {
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
|
||||
private final String path;
|
||||
private final String rawPath;
|
||||
private String path;
|
||||
|
||||
SVG(String path) {
|
||||
this.path = 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 createSVGPath() {
|
||||
var p = new SVGPath();
|
||||
p.setContent(getPath());
|
||||
p.getStyleClass().add("svg");
|
||||
return p;
|
||||
}
|
||||
|
||||
public Node createIcon(double size) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, size);
|
||||
private static Node createIcon(SVGPath path, double size) {
|
||||
if (size == DEFAULT_SIZE)
|
||||
return path;
|
||||
else {
|
||||
double scale = size / DEFAULT_SIZE;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
return new Group(path);
|
||||
}
|
||||
}
|
||||
|
||||
public Node createIcon() {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, -1);
|
||||
return createIcon(DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
public Node createIcon(double size) {
|
||||
return createIcon(createSVGPath(), size);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
SVGPath p = createSVGPath();
|
||||
p.fillProperty().bind(color);
|
||||
return createIcon(p, -1);
|
||||
return createIcon(p, DEFAULT_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.setLeftIcon(SVG.MICROSOFT);
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,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;
|
||||
}
|
||||
@@ -101,19 +100,22 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(item);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
|
||||
SVG unselectedGraphic, SVG selectedGraphic) {
|
||||
AdvancedListItem item = createNavigationDrawerItem(title, null);
|
||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||
item.setOnAction(e -> tabHeader.select(tab));
|
||||
|
||||
Node unselectedIcon = unselectedGraphic.createIcon(20);
|
||||
Node selectedIcon = selectedGraphic.createIcon(20);
|
||||
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
||||
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
||||
|
||||
TransitionPane leftGraphic = new TransitionPane();
|
||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||
leftGraphic.setMouseTransparent(true);
|
||||
leftGraphic.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(leftGraphic, 30);
|
||||
FXUtils.setLimitHeight(leftGraphic, 20);
|
||||
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
|
||||
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||
leftGraphic.setPadding(Insets.EMPTY);
|
||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
||||
FXUtils.onChange(item.activeProperty(), active ->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||
|
||||
DownloadMojangJava() {
|
||||
setTitle(i18n("java.download"));
|
||||
setTitle(i18n("java.download.title"));
|
||||
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
|
||||
|
||||
VBox vbox = new VBox(16);
|
||||
@@ -311,7 +311,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"));
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
+64
-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,26 @@ 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.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 +577,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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,9 +146,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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -587,8 +592,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 {
|
||||
@@ -1041,6 +1054,10 @@
|
||||
-fx-font-size: 12;
|
||||
}
|
||||
|
||||
.options-list-item > .line-component .line-component-container {
|
||||
-fx-padding: 10 16 10 16;
|
||||
}
|
||||
|
||||
.options-list-item .svg {
|
||||
-fx-fill: -monet-on-surface;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
@@ -1076,6 +1093,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 +1128,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 +1294,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 +1375,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 +1434,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 +1883,43 @@
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Line Button *
|
||||
* Line Component *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
|
||||
.line-select-button .svg {
|
||||
-fx-opacity: 1;
|
||||
.line-component:large-title .title-label {
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.line-select-button .svg:disabled {
|
||||
.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;
|
||||
}
|
||||
@@ -390,12 +390,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 +465,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
|
||||
@@ -733,10 +739,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 +761,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 +1069,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 +1089,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 +1110,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 +1133,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 +1143,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 +1170,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 +1186,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 +1254,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 +1264,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 +1404,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 +1633,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 +1641,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
|
||||
|
||||
@@ -363,11 +363,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 +434,6 @@ folder.logs=السجلات
|
||||
folder.mod=التعديلات
|
||||
folder.resourcepacks=حزم الموارد
|
||||
folder.shaderpacks=حزم الظلال
|
||||
folder.saves=الحفظ
|
||||
folder.schematics=المخططات
|
||||
folder.screenshots=لقطات الشاشة
|
||||
folder.world=دليل العالم
|
||||
@@ -703,7 +705,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 +727,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 +1031,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 +1048,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 +1084,6 @@ nbt.save.failed=فشل حفظ الملف
|
||||
nbt.title=عرض الملف - %s
|
||||
|
||||
datapack=حزم البيانات
|
||||
datapack.add=تثبيت حزمة بيانات
|
||||
datapack.choose_datapack=اختر حزمة بيانات للاستيراد
|
||||
datapack.extension=حزمة بيانات
|
||||
datapack.title=عالم [%s] - حزم البيانات
|
||||
|
||||
web.failed=فشل تحميل الصفحة
|
||||
@@ -1097,9 +1091,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 +1109,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 +1181,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 +1312,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 +1524,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 +1531,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=حذف جميع المكتبات
|
||||
|
||||
@@ -365,11 +365,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\
|
||||
@@ -710,10 +713,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 +735,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 +1039,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 +1057,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 +1078,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 +1091,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 +1098,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 +1119,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 +1191,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 +1321,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 +1458,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
|
||||
|
||||
@@ -315,11 +315,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 +341,6 @@ folder.game=ゲームディレクトリ
|
||||
folder.mod=Mod
|
||||
folder.resourcepacks=リソースパック
|
||||
folder.shaderpacks=シェーダーパックフォルダー
|
||||
folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
@@ -440,7 +442,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 +459,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 +659,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 +671,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 +689,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 +946,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=このバージョンの名前を変更します
|
||||
|
||||
@@ -361,11 +361,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。
|
||||
@@ -511,7 +514,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 +835,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 +852,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 +887,7 @@ nbt.title=覽案 - %s
|
||||
|
||||
datapack=錄囊
|
||||
datapack.add=增錄囊
|
||||
datapack.choose_datapack=擇須導入之錄囊緊囊
|
||||
datapack.extension=錄囊
|
||||
datapack.add.title=擇須導入之錄囊緊囊
|
||||
datapack.title=生界 [%s] - 錄囊
|
||||
|
||||
web.failed=載網葉未成
|
||||
@@ -895,6 +896,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 +922,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=允令 (舞弊)
|
||||
|
||||
@@ -365,11 +365,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\
|
||||
@@ -701,10 +704,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 +726,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 +1032,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 +1050,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 +1071,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 +1084,6 @@ nbt.save.failed=Не удалось сохранить файл
|
||||
nbt.title=Смотреть файл - %s
|
||||
|
||||
datapack=Наборы данных
|
||||
datapack.add=Установить
|
||||
datapack.choose_datapack=Выберите набор данных для импорта
|
||||
datapack.extension=Набор данных
|
||||
datapack.title=Мир [%s] - Наборы данных
|
||||
|
||||
web.failed=Не удалось загрузить страницу
|
||||
@@ -1096,9 +1091,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 +1112,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 +1183,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 +1312,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=Значок
|
||||
|
||||
|
||||
@@ -363,11 +363,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\
|
||||
@@ -650,10 +653,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 +675,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 +977,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 +995,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 +1016,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 +1029,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 +1036,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 +1057,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 +1129,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 +1257,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 +1387,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 +1394,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=Перейменувати екземпляр
|
||||
|
||||
@@ -120,7 +120,7 @@ account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft
|
||||
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
|
||||
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
|
||||
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
|
||||
account.methods.microsoft.profile=編輯帳戶個人信息
|
||||
account.methods.microsoft.profile=編輯帳戶個人資訊
|
||||
account.methods.microsoft.purchase=購買 Minecraft
|
||||
account.methods.forgot_password=忘記密碼
|
||||
account.methods.ban_query=查詢帳戶是否被封禁
|
||||
@@ -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 來完成升級。
|
||||
@@ -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=正在初始化聯機核心
|
||||
|
||||
@@ -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。
|
||||
@@ -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=请输入新文件夹名称
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
+4
-10
@@ -21,7 +21,6 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.UnsupportedInstallationException;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.Artifact;
|
||||
@@ -31,9 +30,10 @@ import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.download.UnsupportedInstallationException.FABRIC_NOT_COMPATIBLE_WITH_FORGE;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class LegacyFabricInstallTask extends Task<Version> {
|
||||
|
||||
@@ -57,12 +57,6 @@ public final class LegacyFabricInstallTask extends Task<Version> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
if (!Objects.equals("net.minecraft.client.main.Main", version.resolve(dependencyManager.getGameRepository()).getMainClass()))
|
||||
throw new UnsupportedInstallationException(FABRIC_NOT_COMPATIBLE_WITH_FORGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singleton(launchMetaTask);
|
||||
|
||||
@@ -128,7 +128,7 @@ public final class GameDumpGenerator {
|
||||
}
|
||||
|
||||
private static void writeDumpBodyTo(VirtualMachine vm, Writer writer) throws IOException {
|
||||
execute(vm, "Thread.print", writer);
|
||||
execute(vm, "Thread.print -e -l", writer);
|
||||
}
|
||||
|
||||
private static VirtualMachine attachVM(String lvmid, Writer writer) throws IOException, InterruptedException {
|
||||
|
||||
@@ -159,7 +159,7 @@ public final class RemoteMod {
|
||||
if (this.type == DependencyType.BROKEN) {
|
||||
this.remoteMod = RemoteMod.BROKEN;
|
||||
} else {
|
||||
this.remoteMod = this.remoteModRepository.getModById(this.id);
|
||||
this.remoteMod = this.remoteModRepository.resolveDependency(this.id);
|
||||
}
|
||||
}
|
||||
return this.remoteMod;
|
||||
|
||||
@@ -92,6 +92,10 @@ public interface RemoteModRepository {
|
||||
|
||||
RemoteMod getModById(String id) throws IOException;
|
||||
|
||||
default RemoteMod resolveDependency(String id) throws IOException {
|
||||
return getModById(id);
|
||||
}
|
||||
|
||||
RemoteMod.File getModFile(String modId, String fileId) throws IOException;
|
||||
|
||||
Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException;
|
||||
|
||||
+15
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
@@ -154,6 +155,20 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod resolveDependency(String id) throws IOException {
|
||||
try {
|
||||
return getModById(id);
|
||||
} catch (ResponseCodeException e) {
|
||||
if (e.getResponseCode() == 502 || e.getResponseCode() == 404) {
|
||||
return RemoteMod.BROKEN;
|
||||
}
|
||||
throw e;
|
||||
} catch (FileNotFoundException e) {
|
||||
return RemoteMod.BROKEN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@@ -341,13 +341,21 @@ public abstract class Task<T> {
|
||||
|
||||
private long lastUpdateProgressTime = 0L;
|
||||
|
||||
protected void updateProgress(long progress, long total) {
|
||||
updateProgress(1.0 * progress / total);
|
||||
protected void updateProgress(long count, long total) {
|
||||
if (count < 0 || total < 0)
|
||||
throw new IllegalArgumentException("Invalid count or total: count=" + count + ", total=" + total);
|
||||
|
||||
double progress;
|
||||
if (total >= count)
|
||||
progress = 1.0;
|
||||
else
|
||||
progress = (double) count / total;
|
||||
updateProgress(progress);
|
||||
}
|
||||
|
||||
protected void updateProgress(double progress) {
|
||||
if (progress < 0 || progress > 1.0 || Double.isNaN(progress))
|
||||
throw new IllegalArgumentException("Progress must be between 0 and 1.");
|
||||
throw new IllegalArgumentException("Invalid progress: " + progress);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (progress == 1.0 || now - lastUpdateProgressTime >= 1000L) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,124 +17,121 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import javafx.collections.transformation.TransformationList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
import static javafx.collections.FXCollections.unmodifiableObservableList;
|
||||
/// @author Glavo
|
||||
public final class MappedObservableList<E, F> extends TransformationList<E, F> {
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class MappedObservableList {
|
||||
private MappedObservableList() {
|
||||
/// This method creates a mapping of `source`, using `mapper` as the converter.
|
||||
///
|
||||
/// If an item is added to `source`, `mapper` will be invoked to create a corresponding item, which will also be added to the returned `ObservableList`.
|
||||
/// If an item is removed from `source`, the corresponding item in the returned `ObservableList` will also be removed.
|
||||
/// If `source` is permutated, the returned `ObservableList` will also be permutated in the same way.
|
||||
///
|
||||
/// The returned `ObservableList` is unmodifiable.
|
||||
public static <T, U> ObservableList<U> create(ObservableList<T> source, Function<T, U> mapper) {
|
||||
return new MappedObservableList<>(source, mapper);
|
||||
}
|
||||
|
||||
private static final class MappedObservableListUpdater<T, U> implements ListChangeListener<T> {
|
||||
private final ObservableList<T> origin;
|
||||
private final ObservableList<U> target;
|
||||
private final Function<T, U> mapper;
|
||||
private final Function<? super F, ? extends E> mapper;
|
||||
private final List<E> elements;
|
||||
|
||||
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
|
||||
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
|
||||
private final List<U> buffer;
|
||||
|
||||
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
|
||||
this.origin = origin;
|
||||
this.target = target;
|
||||
this.mapper = mapper;
|
||||
this.buffer = new ArrayList<>(target);
|
||||
public MappedObservableList(@NotNull ObservableList<? extends F> source, @NotNull Function<? super F, ? extends E> mapper) {
|
||||
super(source);
|
||||
this.mapper = mapper;
|
||||
this.elements = new ArrayList<>(source.size());
|
||||
for (F f : source) {
|
||||
elements.add(mapper.apply(f));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(Change<? extends T> change) {
|
||||
// cache removed elements to reduce calls to mapper
|
||||
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void sourceChanged(ListChangeListener.Change<? extends F> change) {
|
||||
beginChange();
|
||||
while (change.next()) {
|
||||
int from = change.getFrom();
|
||||
int to = change.getTo();
|
||||
|
||||
while (change.next()) {
|
||||
int from = change.getFrom();
|
||||
int to = change.getTo();
|
||||
if (change.wasPermutated()) {
|
||||
Object[] temp = new Object[to - from];
|
||||
int[] permutations = new int[to - from];
|
||||
|
||||
if (change.wasPermutated()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
U[] temp = (U[]) new Object[to - from];
|
||||
for (int i = 0; i < temp.length; i++) {
|
||||
temp[i] = buffer.get(from + i);
|
||||
}
|
||||
for (int i = 0; i < temp.length; i++) {
|
||||
temp[i] = elements.get(from + i);
|
||||
}
|
||||
|
||||
for (int idx = from; idx < to; idx++) {
|
||||
buffer.set(change.getPermutation(idx), temp[idx - from]);
|
||||
}
|
||||
} else {
|
||||
if (change.wasRemoved()) {
|
||||
List<? extends T> originRemoved = change.getRemoved();
|
||||
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
|
||||
for (int i = 0; i < targetRemoved.size(); i++) {
|
||||
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
|
||||
}
|
||||
targetRemoved.clear();
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
U[] toAdd = (U[]) new Object[to - from];
|
||||
for (int i = 0; i < toAdd.length; i++) {
|
||||
toAdd[i] = map(cache, origin.get(from + i));
|
||||
}
|
||||
buffer.addAll(from, Arrays.asList(toAdd));
|
||||
for (int i = from; i < to; i++) {
|
||||
int n = i - from;
|
||||
int permutation = change.getPermutation(i);
|
||||
permutations[n] = permutation;
|
||||
elements.set(permutation, (E) temp[n]);
|
||||
}
|
||||
|
||||
nextPermutation(from, to, permutations);
|
||||
} else if (change.wasUpdated()) {
|
||||
for (int i = from; i < to; i++) {
|
||||
elements.set(i, mapper.apply(getSource().get(i)));
|
||||
nextUpdate(i);
|
||||
}
|
||||
} else {
|
||||
List<E> removed = List.of();
|
||||
if (change.wasRemoved()) {
|
||||
List<E> subList = elements.subList(from, from + change.getRemovedSize());
|
||||
removed = new ArrayList<>(subList);
|
||||
subList.clear();
|
||||
}
|
||||
|
||||
if (change.wasAdded()) {
|
||||
Object[] temp = new Object[to - from];
|
||||
List<? extends F> addedSubList = change.getAddedSubList();
|
||||
for (int i = 0; i < addedSubList.size(); i++) {
|
||||
temp[i] = mapper.apply(addedSubList.get(i));
|
||||
}
|
||||
elements.addAll(from, (List<E>) Arrays.asList(temp));
|
||||
}
|
||||
|
||||
if (change.wasRemoved() && change.wasAdded()) {
|
||||
nextReplace(from, to, removed);
|
||||
} else if (change.wasRemoved()) {
|
||||
nextRemove(from, removed);
|
||||
} else if (change.wasAdded()) {
|
||||
nextAdd(from, to);
|
||||
}
|
||||
}
|
||||
target.setAll(buffer);
|
||||
}
|
||||
|
||||
private void pushCache(Map<T, LinkedList<U>> cache, T key, U value) {
|
||||
cache.computeIfAbsent(key, any -> new LinkedList<>())
|
||||
.push(value);
|
||||
}
|
||||
|
||||
private U map(Map<T, LinkedList<U>> cache, T key) {
|
||||
LinkedList<U> stack = cache.get(key);
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
return stack.pop();
|
||||
}
|
||||
return mapper.apply(key);
|
||||
}
|
||||
endChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
|
||||
*
|
||||
* If an item is added to {@code origin}, {@code mapper} will be invoked to create a corresponding item, which will also be added to the returned {@code ObservableList}.
|
||||
* If an item is removed from {@code origin}, the corresponding item in the returned {@code ObservableList} will also be removed.
|
||||
* If {@code origin} is permutated, the returned {@code ObservableList} will also be permutated in the same way.
|
||||
*
|
||||
* The returned {@code ObservableList} is unmodifiable.
|
||||
*/
|
||||
public static <T, U> ObservableList<U> create(ObservableList<T> origin, Function<T, U> mapper) {
|
||||
// create a already-synchronized target ObservableList<U>
|
||||
ObservableList<U> target = origin.stream()
|
||||
.map(mapper)
|
||||
.collect(toCollection(FXCollections::observableArrayList));
|
||||
|
||||
// then synchronize further changes to target
|
||||
ListChangeListener<T> listener = new MappedObservableListUpdater<>(origin, target, mapper);
|
||||
|
||||
// let target hold a reference to listener to prevent listener being garbage-collected before target is garbage-collected
|
||||
target.addListener(new Holder<>(listener));
|
||||
|
||||
// let origin hold a weak reference to listener, so that target can be garbage-collected when it's no longer used
|
||||
origin.addListener(new WeakListChangeListener<>(listener));
|
||||
|
||||
// ref graph:
|
||||
// target ------> listener <-weak- origin
|
||||
// <------ ------>
|
||||
|
||||
return unmodifiableObservableList(target);
|
||||
@Override
|
||||
public E get(int index) {
|
||||
return elements.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return elements.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSourceIndex(int index) {
|
||||
Objects.checkIndex(index, this.size());
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewIndex(int index) {
|
||||
Objects.checkIndex(index, this.size());
|
||||
return index;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -348,6 +348,11 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
|
||||
needNormalize = true;
|
||||
releaseType = ReleaseType.PRE_RELEASE;
|
||||
eaVersion = VersionNumber.asVersion(suffix.substring(" Pre-Release ".length()));
|
||||
} else if (suffix.startsWith(" Pre-release ")) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5476
|
||||
needNormalize = true;
|
||||
releaseType = ReleaseType.PRE_RELEASE;
|
||||
eaVersion = VersionNumber.asVersion(suffix.substring(" Pre-release ".length()));
|
||||
} else if (suffix.startsWith("-rc")) {
|
||||
releaseType = ReleaseType.RELEASE_CANDIDATE;
|
||||
eaVersion = VersionNumber.asVersion(suffix.substring("-rc".length()));
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
1.18_experimental-snapshot-6,1.18 Experimental Snapshot 6,1.18 experimental snapshot 6
|
||||
1.18_experimental-snapshot-7,1.18 Experimental Snapshot 7,1.18 experimental snapshot 7
|
||||
1.19_deep_dark_experimental_snapshot-1,Deep Dark Experimental Snapshot 1
|
||||
2.0,2.0_red,2.0_blue,2.0_purple
|
||||
20w14infinite,20w14~
|
||||
22w13oneBlockAtATime,22w13oneblockatatime
|
||||
|
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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.javafx;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableListBase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/// @author Glavo
|
||||
public class MappedObservableListTest {
|
||||
|
||||
@Test
|
||||
public void testInitialMapping() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-2", mapped.get(1));
|
||||
assertEquals("Item-3", mapped.get(2));
|
||||
assertEquals("Item-4", mapped.get(3));
|
||||
assertEquals("Item-5", mapped.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.add(4);
|
||||
assertEquals(4, mapped.size());
|
||||
assertEquals("Item-4", mapped.get(3));
|
||||
|
||||
source.add(1, 10);
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-10", mapped.get(1));
|
||||
assertEquals("Item-2", mapped.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.remove(2);
|
||||
assertEquals(4, mapped.size());
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-2", mapped.get(1));
|
||||
assertEquals("Item-4", mapped.get(2));
|
||||
assertEquals("Item-5", mapped.get(3));
|
||||
|
||||
source.remove(Integer.valueOf(1));
|
||||
assertEquals(3, mapped.size());
|
||||
assertEquals("Item-2", mapped.get(0));
|
||||
assertEquals("Item-4", mapped.get(1));
|
||||
assertEquals("Item-5", mapped.get(2));
|
||||
|
||||
source.remove(1, 3);
|
||||
assertEquals(1, mapped.size());
|
||||
assertEquals("Item-2", mapped.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSet() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.set(2, 10);
|
||||
assertEquals(5, mapped.size());
|
||||
|
||||
// Verify the actual values
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-2", mapped.get(1));
|
||||
assertEquals("Item-10", mapped.get(2));
|
||||
assertEquals("Item-4", mapped.get(3));
|
||||
assertEquals("Item-5", mapped.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSort() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(5, 3, 1, 4, 2);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
FXCollections.sort(source);
|
||||
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-2", mapped.get(1));
|
||||
assertEquals("Item-3", mapped.get(2));
|
||||
assertEquals("Item-4", mapped.get(3));
|
||||
assertEquals("Item-5", mapped.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClear() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.clear();
|
||||
|
||||
assertEquals(0, mapped.size());
|
||||
assertTrue(mapped.isEmpty());
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> mapped.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAll() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.addAll(3, 4, 5);
|
||||
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-3", mapped.get(2));
|
||||
assertEquals("Item-4", mapped.get(3));
|
||||
assertEquals("Item-5", mapped.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAll() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.removeAll(2, 4);
|
||||
|
||||
assertEquals(3, mapped.size());
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-3", mapped.get(1));
|
||||
assertEquals("Item-5", mapped.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAll() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.setAll(10, 20, 30, 40);
|
||||
|
||||
assertEquals(4, mapped.size());
|
||||
assertEquals("Item-10", mapped.get(0));
|
||||
assertEquals("Item-20", mapped.get(1));
|
||||
assertEquals("Item-30", mapped.get(2));
|
||||
assertEquals("Item-40", mapped.get(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSourceIndex() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
MappedObservableList<String, Integer> mapped = new MappedObservableList<>(source, i -> "Item-" + i);
|
||||
|
||||
assertEquals(0, mapped.getSourceIndex(0));
|
||||
assertEquals(2, mapped.getSourceIndex(2));
|
||||
assertEquals(4, mapped.getSourceIndex(4));
|
||||
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getSourceIndex(-1));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getSourceIndex(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetViewIndex() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||
MappedObservableList<String, Integer> mapped = new MappedObservableList<>(source, i -> "Item-" + i);
|
||||
|
||||
assertEquals(0, mapped.getViewIndex(0));
|
||||
assertEquals(2, mapped.getViewIndex(2));
|
||||
assertEquals(4, mapped.getViewIndex(4));
|
||||
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getViewIndex(-1));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getViewIndex(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComplexOperations() {
|
||||
ObservableList<Integer> source = FXCollections.observableArrayList();
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
// Start with empty
|
||||
assertEquals(0, mapped.size());
|
||||
|
||||
// Add some elements
|
||||
source.addAll(1, 2, 3, 4, 5);
|
||||
assertEquals(5, mapped.size());
|
||||
|
||||
// Remove middle element
|
||||
source.remove(2);
|
||||
assertEquals(4, mapped.size());
|
||||
assertEquals("Item-4", mapped.get(2));
|
||||
|
||||
// Sort
|
||||
FXCollections.sort(source, Collections.reverseOrder());
|
||||
assertEquals("Item-5", mapped.get(0));
|
||||
assertEquals("Item-4", mapped.get(1));
|
||||
assertEquals("Item-2", mapped.get(2));
|
||||
assertEquals("Item-1", mapped.get(3));
|
||||
|
||||
// Add at specific position
|
||||
source.add(2, 3);
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-3", mapped.get(2));
|
||||
}
|
||||
|
||||
/// Test for [javafx.collections.ListChangeListener.Change#wasUpdated()].
|
||||
@Test
|
||||
public void testUpdate() {
|
||||
class TestUpdateList<T> extends ObservableListBase<T> {
|
||||
private final List<T> backingList;
|
||||
|
||||
@SafeVarargs
|
||||
public TestUpdateList(T... items) {
|
||||
this.backingList = Arrays.asList(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return backingList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(int index) {
|
||||
return backingList.get(index);
|
||||
}
|
||||
|
||||
public void updateItem(int beginIndex, int endIndex, Function<T, T> mapper) {
|
||||
Objects.checkFromToIndex(beginIndex, endIndex, size());
|
||||
beginChange();
|
||||
for (int i = beginIndex; i < endIndex; i++) {
|
||||
backingList.set(i, mapper.apply(backingList.get(i)));
|
||||
nextUpdate(i);
|
||||
}
|
||||
endChange();
|
||||
}
|
||||
}
|
||||
|
||||
TestUpdateList<Integer> source = new TestUpdateList<>(1, 2, 3, 4, 5);
|
||||
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||
|
||||
source.updateItem(2, 4, i -> i * 10);
|
||||
assertEquals(5, mapped.size());
|
||||
assertEquals("Item-1", mapped.get(0));
|
||||
assertEquals("Item-2", mapped.get(1));
|
||||
assertEquals("Item-30", mapped.get(2));
|
||||
assertEquals("Item-40", mapped.get(3));
|
||||
assertEquals("Item-5", mapped.get(4));
|
||||
}
|
||||
}
|
||||
@@ -450,6 +450,7 @@ public final class GameVersionNumberTest {
|
||||
assertNormalized("1.16_combat-4", "Combat Test 8");
|
||||
assertNormalized("1.16_combat-5", "Combat Test 8b");
|
||||
assertNormalized("1.16_combat-6", "Combat Test 8c");
|
||||
assertNormalized("1.16.2-pre1", "1.16.2 Pre-release 1"); // https://github.com/HMCL-dev/HMCL/pull/5476
|
||||
assertNormalized("1.18_experimental-snapshot-1", "1.18 Experimental Snapshot 1");
|
||||
assertNormalized("1.18_experimental-snapshot-2", "1.18 experimental snapshot 2");
|
||||
assertNormalized("1.18_experimental-snapshot-3", "1.18 experimental snapshot 3");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing Guide
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | [中文](Contributing_zh.md)
|
||||
**English** | 中文 ([简体](Contributing_zh.md), [繁體](Contributing_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Build HMCL
|
||||
@@ -89,7 +89,7 @@ These options can be specified via environment variables or JVM parameters. If b
|
||||
| `HMCL_ANIMATION_FRAME_RATE` | | Specifies the animation frame rate of HMCL | `60` | |
|
||||
| `HMCL_LANGUAGE` | | Specifies the default language of HMCL | Uses the system default language | |
|
||||
| | `-Dhmcl.dir=<path>` | Specifies the current data folder of HMCL | `./.hmcl` | |
|
||||
| | `-Dhmcl.home=<path>` | Specifies the user data folder of HMCL | Windows: `%APPDATA\.hmcl`<br>Linux/BSD: `$XDG_DATA_HOME/hmcl`<br>macOS: `~Library/Application Support/hmcl` | |
|
||||
| | `-Dhmcl.home=<path>` | Specifies the user data folder of HMCL | Windows: `%APPDATA%\.hmcl`<br>Linux/BSD: `$XDG_DATA_HOME/hmcl`<br>macOS: `~Library/Application Support/hmcl` | |
|
||||
| | `-Dhmcl.self_integrity_check.disable=true` | Disables self-integrity checks during updates | | |
|
||||
| | `-Dhmcl.bmclapi.override=<url>` | Specifies the API Root for BMCLAPI | `https://bmclapi2.bangbang93.com` | |
|
||||
| | `-Dhmcl.discoapi.override=<url>` | Specifies the API Root for foojay Disco API | `https://api.foojay.io/disco/v3.0` | |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 贡献指南
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](Contributing.md) | **中文**
|
||||
[English](Contributing.md) | **中文** (**简体**, [繁體](Contributing_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 构建 HMCL
|
||||
@@ -88,7 +88,7 @@ HMCL 提供了一系列调试选项,用于控制启动器的行为。
|
||||
| `HMCL_ANIMATION_FRAME_RATE` | | 指定 HMCL 的动画帧率 | `60` | |
|
||||
| `HMCL_LANGUAGE` | | 指定 HMCL 的默认语言 | 使用系统默认语言 |
|
||||
| | `-Dhmcl.dir=<path>` | 指定 HMCL 的当前数据文件夹 | `./.hmcl` | |
|
||||
| | `-Dhmcl.home=<path>` | 指定 HMCL 的用户数据文件夹 | Windows: `%APPDATA\.hmcl`<br>Linux/BSD: `$XDG_DATA_HOME/hmcl`<br>macOS: `~Library/Application Support/hmcl` | |
|
||||
| | `-Dhmcl.home=<path>` | 指定 HMCL 的用户数据文件夹 | Windows: `%APPDATA%\.hmcl`<br>Linux/BSD: `$XDG_DATA_HOME/hmcl`<br>macOS: `~Library/Application Support/hmcl` | |
|
||||
| | `-Dhmcl.self_integrity_check.disable=true` | 检查更新时不检查本体完整性 | | |
|
||||
| | `-Dhmcl.bmclapi.override=<url>` | 指定 BMCLAPI 的 API Root | `https://bmclapi2.bangbang93.com` | |
|
||||
| | `-Dhmcl.discoapi.override=<url>` | 指定 foojay Disco API 的 API Root | `https://api.foojay.io/disco/v3.0` |
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 貢獻指南
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](Contributing.md) | **中文** ([简体](Contributing_zh.md), **繁體**)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 構建 HMCL
|
||||
|
||||
### 環境需求
|
||||
|
||||
構建 HMCL 啟動器需要安裝 JDK 17 (或更高版本)。你可以從此處下載它: [Download Liberica JDK](https://bell-sw.com/pages/downloads/#jdk-25-lts)。
|
||||
|
||||
在安裝 JDK 後,請確保 `JAVA_HOME` 環境變數指向符合需求的 JDK 目錄。
|
||||
你可以這樣查看 `JAVA_HOME` 指向的 JDK 版本:
|
||||
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
|
||||
PowerShell:
|
||||
```
|
||||
PS > & "$env:JAVA_HOME/bin/java.exe" -version
|
||||
openjdk version "25" 2025-09-16 LTS
|
||||
OpenJDK Runtime Environment (build 25+37-LTS)
|
||||
OpenJDK 64-Bit Server VM (build 25+37-LTS, mixed mode, sharing)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux/FreeBSD</summary>
|
||||
|
||||
```
|
||||
> $JAVA_HOME/bin/java -version
|
||||
openjdk version "25" 2025-09-16 LTS
|
||||
OpenJDK Runtime Environment (build 25+37-LTS)
|
||||
OpenJDK 64-Bit Server VM (build 25+37-LTS, mixed mode, sharing)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>macOS</summary>
|
||||
|
||||
```
|
||||
> /usr/libexec/java_home --exec java -version
|
||||
openjdk version "25" 2025-09-16 LTS
|
||||
OpenJDK Runtime Environment (build 25+37-LTS)
|
||||
OpenJDK 64-Bit Server VM (build 25+37-LTS, mixed mode, sharing)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 獲取 HMCL 原始碼
|
||||
|
||||
- 透過 [Git](https://git-scm.com/downloads) 可以獲取最新原始碼:
|
||||
```shell
|
||||
git clone https://github.com/HMCL-dev/HMCL.git
|
||||
cd HMCL
|
||||
```
|
||||
- 從 [GitHub Release 頁面](https://github.com/HMCL-dev/HMCL/releases)可以手動下載特定版本的原始碼。
|
||||
|
||||
### 構建 HMCL
|
||||
|
||||
想要構建 HMCL,請切換到 HMCL 專案的根目錄下,並執行以下指令:
|
||||
|
||||
```shell
|
||||
./gradlew clean makeExecutables
|
||||
```
|
||||
|
||||
構建出的 HMCL 程式檔位於根目錄下的 `HMCL/build/libs` 子目錄中。
|
||||
|
||||
## 除錯選項
|
||||
|
||||
> [!WARNING]
|
||||
> 本文介紹的是 HMCL 的內部功能,我們不保證這些功能的穩定性,並且隨時可能修改或刪除這些功能。
|
||||
>
|
||||
> 使用這些功能時請務必小心,錯誤地使用這些功能可能會導致 HMCL 行為異常甚至崩潰。
|
||||
|
||||
HMCL 提供了一系列除錯選項,用於控制啟動器的行為。
|
||||
|
||||
這些選項可以透過環境變數或 JVM 參數設定。如果兩者同時存在,那麼 JVM 參數會覆蓋環境變數的設定。
|
||||
|
||||
| 環境變數 | JVM 參數 | 功能 | 預設值 | 額外說明 |
|
||||
|-----------------------------|----------------------------------------------|--------------------------------|-------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| `HMCL_JAVA_HOME` | | 設定用於開啟 HMCL 的 Java | | 僅對 exe/sh 生效 |
|
||||
| `HMCL_JAVA_OPTS` | | 設定開啟 HMCL 時的預設 JVM 參數 | | 僅對 exe/sh 生效 |
|
||||
| `HMCL_FORCE_GPU` | | 設定是否強制使用 GPU 加速繪製 | `false` |
|
||||
| `HMCL_ANIMATION_FRAME_RATE` | | 設定 HMCL 的動畫幀率 | `60` | |
|
||||
| `HMCL_LANGUAGE` | | 設定 HMCL 的預設語言 | 使用系統預設語言 |
|
||||
| | `-Dhmcl.dir=<path>` | 設定 HMCL 的目前資料存放位置 | `./.hmcl` | |
|
||||
| | `-Dhmcl.home=<path>` | 設定 HMCL 的使用者資料存放位置 | Windows: `%APPDATA%\.hmcl`<br>Linux/BSD: `$XDG_DATA_HOME/hmcl`<br>macOS: `~Library/Application Support/hmcl` | |
|
||||
| | `-Dhmcl.self_integrity_check.disable=true` | 檢查更新時不檢查程式完整性 | | |
|
||||
| | `-Dhmcl.bmclapi.override=<url>` | 設定 BMCLAPI 的 API Root | `https://bmclapi2.bangbang93.com` | |
|
||||
| | `-Dhmcl.discoapi.override=<url>` | 設定 foojay Disco API 的 API Root | `https://api.foojay.io/disco/v3.0` |
|
||||
| `HMCL_FONT` | `-Dhmcl.font.override=<font family>` | 設定 HMCL 預設字體 | 使用系統預設字體 | |
|
||||
| | `-Dhmcl.update_source.override=<url>` | 設定 HMCL 更新來源 | `https://hmcl.huangyuhui.net/api/update_link` | |
|
||||
| | `-Dhmcl.authlibinjector.location=<path>` | 設定 authlib-injector JAR 檔的位置 | 使用 HMCL 內置的 authlib-injector | |
|
||||
| | `-Dhmcl.openjfx.repo=<maven repository url>` | 添加用於下載 OpenJFX 的自訂 Maven 倉庫 | | |
|
||||
| | `-Dhmcl.native.encoding=<encoding>` | 設定原生編碼 | 使用系統的本機編碼 | |
|
||||
| | `-Dhmcl.microsoft.auth.id=<App ID>` | 設定 Microsoft OAuth App ID | 使用 HMCL 內建的 Microsoft OAuth App ID | |
|
||||
| | `-Dhmcl.microsoft.auth.secret=<App Secret>` | 設定 Microsoft OAuth App 金鑰 | 使用 HMCL 內建的 Microsoft OAuth App 金鑰 | |
|
||||
| | `-Dhmcl.curseforge.apikey=<Api Key>` | 設定 CurseForge API 金鑰 | 使用 HMCL 內建的 CurseForge API 金鑰 | |
|
||||
| | `-Dhmcl.native.backend=<auto/jna/none>` | 設定 HMCL 使用的本機後端 | `auto` |
|
||||
| | `-Dhmcl.hardware.fastfetch=<true/false>` | 設定是否使用 fastfetch 檢測硬體資訊 | `true` |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Localization
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | [中文](Localization_zh.md)
|
||||
**English** | 中文 ([简体](Localization_zh.md), [繁體](Localization_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
HMCL provides localization support for multiple languages.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 本地化
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](Localization.md) | **中文**
|
||||
[English](Localization.md) | **中文** (**简体**, [繁體](Localization_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
HMCL 为多种语言提供本地化支持。
|
||||
@@ -75,7 +75,7 @@ HMCL 会要求所有 Pull Request 在更新文档和本地化资源时同步更
|
||||
而对于**次要**支持的语言,我们不能保证这些本地化资源总是会同步更新。
|
||||
我们需要熟练掌握这些语言的协作者帮助我们进行维护。
|
||||
|
||||
我们会在文档中记录愿意帮助我们维护这些语言本地化资源的的志愿者。
|
||||
我们会在文档中记录愿意帮助我们维护这些语言本地化资源的志愿者。
|
||||
如果贡献者希望及时将新增的本地化文本翻译至这些语言,
|
||||
那么可以在 PR 中 @ 这些志愿者寻求帮助。
|
||||
|
||||
@@ -210,8 +210,8 @@ HMCL 的维护者会替你完成其他步骤。
|
||||
1. `en-Latn-US`
|
||||
2. `en-Latn`
|
||||
3. `en-US`
|
||||
2. `en`
|
||||
3. `und`
|
||||
4. `en`
|
||||
5. `und`
|
||||
|
||||
对于语言标签 `zh-CN`,HMCL 会将其细化为 `cmn-Hans-CN`,并据此推导出以下搜索列表:
|
||||
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
# 在地化
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](Localization.md) | **中文** ([简体](Localization_zh.md), **繁體**)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
HMCL 為多種語言提供在地化支援。
|
||||
|
||||
本文件介紹了 HMCL 對這些語言的支援狀態,並給想要為 HMCL 的本地化工作做出貢獻的貢獻者提供了一份指南。
|
||||
|
||||
## 支援的語言
|
||||
|
||||
目前,HMCL 為這些語言提供支援:
|
||||
|
||||
| 語言 | 語言標籤 | 首選在地化機碼 | 首選在地化檔案後綴 | [遊戲語言檔案](https://minecraft.wiki/w/Language) | 支援狀態 | 志願者 |
|
||||
|---------|-----------|-----------|------------|---------------------------------------------|--------|-------------------------------------------|
|
||||
| 英語 | `en` | `default` | (空) | `en_us` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 英語 (顛倒) | `en-Qabs` | `en-Qabs` | `en_Qabs` | `en_ud` | 自動 | |
|
||||
| 中文 (簡體) | `zh-Hans` | `zh` | `_zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 中文 (繁體) | `zh-Hant` | `zh-Hant` | `_zh_Hant` | `zh_tw` <br/> `zh_hk` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 中文 (文言) | `lzh` | `lzh` | `_lzh` | `lzh` | 次要 | |
|
||||
| 日語 | `ja` | `ja` | `_ja` | `ja_jp` | 次要 | |
|
||||
| 西班牙語 | `es` | `es` | `_es` | `es_es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
||||
| 俄語 | `ru` | `ru` | `_ru` | `ru_ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
||||
| 烏克蘭語 | `uk` | `uk` | `_uk` | `uk_ua` | 次要 | |
|
||||
|
||||
<details>
|
||||
<summary>關於語言標籤</summary>
|
||||
|
||||
HMCL 使用符合 IETF BCP 47 規範的語言標籤。
|
||||
|
||||
在選取語言標籤時,我們會遵循以下原則:
|
||||
|
||||
1. 對於 ISO 639 標準中定義的語言,如果已經在 [IANA 語言子標籤登錄檔](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)中註冊,我們總是使用經過註冊的標籤。
|
||||
|
||||
例如,對於英語,我們使用 `en` 而不是 `eng` 作為語言代碼。
|
||||
|
||||
2. 對於 Minecraft 所定義的非標準語言,應當優先使用語言檔案的 `language.code` 中定義的程式碼,而非遊戲語言檔案的名稱。
|
||||
|
||||
這是因為 Minecraft 有時候會用現實中實際存在的國家/地區代碼來表示虛構語言 (比如說海盜英語的語言檔案為 `en_pt`,但 `PT` 其實是葡萄牙的國家代碼)。
|
||||
|
||||
例如,對於顛倒的英語,我們使用 `en-Qabs` 作為語言代碼,而不是 `en-UD`。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>關於在地化機碼和在地化檔案後綴</summary>
|
||||
|
||||
在地化檔案後綴和在地化機碼用於為[在地化資源](#在地化資源)命名。
|
||||
|
||||
通常來說,在地化機碼就是這份在地化資源對應的語言代碼,而在地化檔案後綴是將語言代碼中的 `-` 取代為 `_`,並加上一個前綴底線得到的。
|
||||
|
||||
作為特例,對於預設的資源,在地化機碼為 `default`,在地化檔案後綴為空。
|
||||
|
||||
由於[資源回退機制](#資源回退機制)的存在。
|
||||
如果沒有完全匹配目前語言環境的資源,HMCL 會根據目前環境的語言標籤推導出一個搜尋列表,根據該列表依次搜尋資源。
|
||||
|
||||
我們建議在提供在地化資源時,總是提供預設資源 (對應 `default` 在地化機碼和空的在地化檔案後綴),
|
||||
以確保所有使用者都能正常載入資源。
|
||||
|
||||
並且我們建議盡可能為在地化資源使用更寬泛的語言標籤,使使用者更不容易回退到預設資源上。
|
||||
|
||||
例如,如果你提供了一份簡體中文的在地化資源,那麼我們推薦使用 `zh` 作為在地化機碼,而不是更具體的 `zh-Hans`,
|
||||
這樣它會對於所有使用中文的使用者生效,避免對於這些使用者回退到預設資源上。
|
||||
|
||||
如果你想同時提供簡體中文和繁體中文的資源,那麼推薦對使用者占比更高的資源使用更寬泛的 `zh` 作為在地化機碼,使其作為預設的中文資源,
|
||||
而對使用者占比更低的資源使用更具體的 `zh-Hans`/`zh-Hant` 作為在地化機碼。
|
||||
|
||||
</details>
|
||||
|
||||
HMCL 會要求所有 Pull Request 在更新文件和在地化資源時同步更新所有**主要**支援的語言對應的資源。
|
||||
如果 PR 作者對相關語言並不了解,那麼可以直接在評論中提出翻譯請求,
|
||||
維護者會在合併 PR 前幫助 PR 作者翻譯這些文字。
|
||||
|
||||
而對於**次要**支援的語言,我們不能保證這些在地化資源總是會同步更新。
|
||||
我們需要熟練掌握這些語言的協作者幫助我們進行維護。
|
||||
|
||||
我們會在文件中記錄願意幫助我們維護這些語言在地化資源的志願者。
|
||||
如果貢獻者希望及時將新增的在地化文字翻譯至這些語言,
|
||||
那麼可以在 PR 中 @ 這些志願者尋求幫助。
|
||||
|
||||
如果你願意幫助我們維護一些語言的在地化資源,那麼請打開一個 PR,
|
||||
將自己的 GitHub 使用者名稱加入上方的志願者列表。
|
||||
我們非常感謝你的幫助。
|
||||
|
||||
## 新增新的語言支援
|
||||
|
||||
HMCL 歡迎任何人參與翻譯和貢獻。但是維護更多語言的翻譯需要付出更多維護成本,所以我們對新增加的語言有一些要求。
|
||||
請在貢獻前確認以下要求:
|
||||
|
||||
- 我們優先考慮 [Minecraft 官方支援的語言](https://minecraft.wiki/w/Language)。
|
||||
如果沒有特殊理由,我們不會為 Minecraft 官方尚未支援的語言提供支援。
|
||||
- 我們希望對所有語言都提供長久的維護支援。
|
||||
由於本項目的維護者們擅長的語言有限,為了避免對新語言的支援很快就因無人維護而過時,
|
||||
我們希望能夠找到擅長該語言者幫助我們長期維護新增的在地化檔案。
|
||||
如果可能缺少長期維護者,我們會更慎重地考慮是否要加入對該語言的支援。
|
||||
|
||||
我們建議貢獻者在提供新語言翻譯之前先透過 [Issue](https://github.com/HMCL-dev/HMCL/issues/new?template=feature.yml) 提出一個功能請求,
|
||||
與其他貢獻者先進行討論,確定了未來的維護方式後再進行翻譯工作。
|
||||
|
||||
### 開始翻譯
|
||||
|
||||
如果你想為 HMCL 新增新的語言支援,請從翻譯 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties) 開始。
|
||||
HMCL 的絕大多數文字都位於這個檔案中,翻譯此檔案就能翻譯整個介面。
|
||||
|
||||
這是一個 Java Properties 檔案,格式非常簡單。
|
||||
在翻譯前請先閱讀該格式的介紹: [Properties 檔案](https://en.wikipedia.org/wiki/.properties)。
|
||||
|
||||
作為翻譯的第一步,請從[這張表格](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)中查詢這個語言對應的兩字母或三字母語言標籤。
|
||||
例如,英語的語言標籤為 `en`。
|
||||
|
||||
在確定了語言標籤後,請在 [`I18N.properties` 檔案旁](../HMCL/src/main/resources/assets/lang)建立 `I18N_<在地化檔案後綴>.properties` (例如 `I18N_en.properties`) 文件。
|
||||
隨後,你就可以開始在這個檔案中進行翻譯工作了。
|
||||
|
||||
`I18N.properties` 檔案會遵循[資源回退機制](#資源回退機制)查詢缺失的譯文。
|
||||
也就是說,你可以逐條目進行翻譯,而你尚未翻譯的條目會自動回退到英語上。
|
||||
|
||||
在翻譯了一部分後,你可以[自行構建 HMCL](./README_zh.md#編譯),編譯出的 HMCL 中就會包含你的譯文。
|
||||
如果你的電腦預設環境不是該語言,你可以將環境變數 `HMCL_LANGUAGE` 指定為你剛剛從表格中找到的語言標籤,
|
||||
HMCL 會自動切換至這個語言。
|
||||
|
||||
到這裡,你就可以把檔案推送到 GitHub上,並向 HMCL 提交 PR 了。
|
||||
HMCL 的維護者會替你完成其他步驟。
|
||||
|
||||
## 在地化資源
|
||||
|
||||
所有文件和在地化資源檔案的命名規則為 `<資源名><在地化檔案後綴>.<副檔名>`。
|
||||
|
||||
例如,對於 `README.md` 來說,不同語言的在地化版本命名如下:
|
||||
|
||||
- 英語: `README.md`
|
||||
- 中文 (簡體): `README_zh.md`
|
||||
- 中文 (繁體): `README_zh_Hant.md`
|
||||
- 中文 (文言): `README_lzh.md`
|
||||
|
||||
除了在地化檔案,HMCL 還支援在地化 JSON 中的部分文字欄位。JSON 中的在地化文字使用以下格式:
|
||||
|
||||
```json5
|
||||
{
|
||||
"<在地化機碼 1>": "<在地化文字 1>",
|
||||
"<在地化機碼 2>": "<在地化文字 2>",
|
||||
// ...
|
||||
"<在地化機碼 N>": "<在地化文字 N>"
|
||||
}
|
||||
```
|
||||
|
||||
例如,對於以下文字欄位:
|
||||
|
||||
```json
|
||||
{
|
||||
"meow": "Meow"
|
||||
}
|
||||
```
|
||||
|
||||
可以將其改寫為在地化文字:
|
||||
|
||||
```json
|
||||
{
|
||||
"meow": {
|
||||
"default": "Meow",
|
||||
"zh": "喵呜",
|
||||
"zh-Hant": "喵嗚"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 資源回退機制
|
||||
|
||||
對於某個語言下的缺失的資源,HMCL 支援一套資源回退機制,會根據不同的語言標籤推導出一個搜尋列表,
|
||||
根據該列表依次搜尋資源。
|
||||
|
||||
在搜尋前,我們會先透過以下步驟對語言標籤進行細化推導。
|
||||
|
||||
1. 歸一化語言代碼
|
||||
|
||||
如果目前語言標籤中的語言代碼子標籤未在 IANA 語言子標籤登錄檔中進行註冊,HMCL 會先嘗試將其映射為登錄檔中已註冊的標籤。
|
||||
|
||||
例如,HMCL會將語言代碼 `eng` 取代為 `en`。
|
||||
|
||||
2. 映射巨集語言至子語言
|
||||
|
||||
如果目前語言代碼是一個 [ISO 639 巨集語言](https://en.wikipedia.org/wiki/ISO_639_macrolanguage),
|
||||
且該巨集語言通常指代某個個體語言,HMCL 會將其取代為該個體語言。
|
||||
|
||||
例如 `zh` (中文) 通常實際指代 `cmn` (國語),所以我們會將語言代碼 `zh` 取代為 `cmn`。
|
||||
|
||||
3. 推導拼寫指令碼
|
||||
|
||||
如果目前語言標籤中未指定拼寫指令碼,HMCL 會依次根據以下規則嘗試推導拼寫指令碼:
|
||||
|
||||
1. 如果目前語言標籤指定了語言變體,該語言變體已在 IANA 語言子標籤登錄檔中,
|
||||
且登錄檔中其所有 `Prefix` 都包含相同的拼寫指令碼,則將目前拼寫指令碼指定為該指令碼。
|
||||
|
||||
例如,如果目前語言變體為 `pinyin` (漢語拼音),則目前拼寫指令碼會被指定為 `Latn` (拉丁文)。
|
||||
|
||||
2. 如果目前語言代碼在 IANA 語言子標籤登錄檔中被指定了 `Suppress-Script`,則將目前拼寫指令碼指定為該指令碼。
|
||||
|
||||
例如,如果目前語言代碼為 `en` (英語),則目前拼寫指令碼會被指定為 `Latn` (拉丁文);
|
||||
如果目前語言代碼為 `ru` (俄語),則目前拼寫指令碼會被指定為 `Cyrl` (西里爾文)。
|
||||
|
||||
3. 如果目前語言代碼是 `lzh` (文言),則將目前拼寫指令碼指定為 `Hant` (繁體漢字)。
|
||||
|
||||
4. 如果目前語言代碼是 `zh` 或 `zh` 的子語言,則檢查目前國家/地區代碼是否為 `TW`、`HK`、`MO` 之一。
|
||||
如果結果為真,則將目前拼寫指令碼指定為 `Hant` (繁體漢字);否則將目前拼寫指令碼指定為 `Hans` (簡體漢字)。
|
||||
|
||||
在對語言代碼細化推導完成後,HMCL 會開始根據此語言標籤推導出一個語言標籤列表。
|
||||
|
||||
例如,對於語言標籤 `en-US`,HMCL 會將其細化為 `en-Latn-US`,並據此推導出以下搜尋列表:
|
||||
|
||||
1. `en-Latn-US`
|
||||
2. `en-Latn`
|
||||
3. `en-US`
|
||||
4. `en`
|
||||
5. `und`
|
||||
|
||||
對於語言標籤 `zh-CN`,HMCL 會將其細化為 `cmn-Hans-CN`,並據此推導出以下搜尋列表:
|
||||
|
||||
1. `cmn-Hans-CN`
|
||||
2. `cmn-Hans`
|
||||
3. `cmn-CN`
|
||||
4. `cmn`
|
||||
5. `zh-Hans-CN`
|
||||
6. `zh-Hans`
|
||||
7. `zh-CN`
|
||||
8. `zh`
|
||||
9. `und`
|
||||
|
||||
對於能夠混合的資源 (例如 `.properties` 檔案),HMCL 會根據此列表的優先度混合資源;
|
||||
對於難以混合的資源 (例如字體檔案),HMCL 會根據此列表載入找到的最高優先度的資源。
|
||||
|
||||
如果目前語言使用 ISO 639 標準的三字元標籤,但同時也存在對應的兩字元標籤,那麼 HMCL 會將其映射至兩字元後再搜尋資源。
|
||||
|
||||
例如,如果目前環境的語言標籤為 `eng-US`,那麼 HMCL 會將其映射至 `en-US` 後再根據上述規則搜尋在地化資源。
|
||||
|
||||
### 對於中文的額外規則
|
||||
|
||||
HMCL 總是會將 `zh-CN` 加入所有中文環境的搜尋列表中,將 `zh-TW` 加入所有繁體中文環境的搜尋列表中。
|
||||
|
||||
以下是幾個常見中文環境對應的在地化資源搜尋列表。
|
||||
|
||||
- `zh-CN`:
|
||||
1. `cmn-Hans-CN`
|
||||
2. `cmn-Hans`
|
||||
3. `cmn-CN`
|
||||
4. `cmn`
|
||||
5. `zh-Hans-CN`
|
||||
6. `zh-Hans`
|
||||
7. `zh-CN`
|
||||
8. `zh`
|
||||
9. `und`
|
||||
- `zh-SG`:
|
||||
1. `cmn-Hans-SG`
|
||||
2. `cmn-Hans`
|
||||
3. `cmn-SG`
|
||||
4. `cmn`
|
||||
5. `zh-Hans-SG`
|
||||
6. `zh-Hans`
|
||||
7. `zh-SG`
|
||||
8. `zh-CN`
|
||||
9. `zh`
|
||||
10. `und`
|
||||
- `zh-TW`:
|
||||
1. `zh-Hant-TW`
|
||||
2. `zh-Hant`
|
||||
3. `zh-TW`
|
||||
4. `zh`
|
||||
5. `zh-CN`
|
||||
6. `und`
|
||||
- `zh-HK`:
|
||||
1. `cmn-Hant-HK`
|
||||
2. `cmn-Hant`
|
||||
3. `cmn-HK`
|
||||
4. `cmn`
|
||||
5. `zh-Hant-HK`
|
||||
6. `zh-Hant`
|
||||
7. `zh-HK`
|
||||
8. `zh-TW`
|
||||
9. `zh`
|
||||
10. `zh-CN`
|
||||
11. `und`
|
||||
- `lzh`:
|
||||
1. `lzh-Hant`
|
||||
2. `lzh`
|
||||
3. `zh-Hant`
|
||||
4. `zh-TW`
|
||||
5. `zh`
|
||||
6. `zh-CN`
|
||||
7. `und`
|
||||
|
||||
## 自動同步文件內容
|
||||
|
||||
<!-- #BEGIN BLOCK -->
|
||||
<!-- #PROPERTY PROCESS_LINK=false -->
|
||||
為了簡化文件的維護,HMCL 使用了一套巨集機制自動維護文件的部分內容。在命令列中執行
|
||||
|
||||
```bash
|
||||
./gradlew updateDocuments
|
||||
```
|
||||
|
||||
即可自動更新所有文件內容。
|
||||
|
||||
例如,為了建立在同一文件的不同語言譯文之間跳轉的連結,請在文件的標題下新增以下內容:
|
||||
|
||||
```markdown
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
```
|
||||
|
||||
隨後執行 `./gradlew updateDocuments`,這兩行內容會被自動取代為類似這樣的跳轉連結:
|
||||
|
||||
```markdown
|
||||
**English** (**Standard**, [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
```
|
||||
|
||||
關於巨集的更多內容,請見 [MacroProcessor.java](../buildSrc/src/main/java/org/jackhuang/hmcl/gradle/docs/MacroProcessor.java)。
|
||||
<!-- #END BLOCK -->
|
||||
@@ -158,7 +158,7 @@
|
||||
* `/`: 不支援的平臺
|
||||
|
||||
我們目前還沒有打算支援這些平臺,主要是因為我們沒有測試這些平臺的裝置。
|
||||
如果你能幫助我們進行測試,請透過提交 Issue 提出支援請求。
|
||||
如果你能幫助我們進行測試,請透過 Issue 提出支援請求。
|
||||
|
||||
## 陶瓦聯機相容性
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
|
||||
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
|
||||
|
||||
HMCL has amazing cross-platform capabilities. Not only does it run on different operating systems like Windows, Linux, macOS, and FreeBSD, but it also supports various CPU architectures such as x86, ARM, RISC-V, MIPS, and LoongArch. You can easily enjoy Minecraft across different platforms through HMCL.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
˙˥ƆWH ɥƃnoɹɥʇ sɯɹoɟʇɐld ʇuǝɹǝɟɟᴉp ssoɹɔɐ ʇɟɐɹɔǝuᴉW ʎoɾuǝ ʎlᴉsɐǝ uɐɔ no⅄ ˙ɥɔɹ∀ƃuoo˥ puɐ 'SԀIW 'Λ-ƆSIɹ 'Wɹ∀ '98x sɐ ɥɔns sǝɹnʇɔǝʇᴉɥɔɹɐ ∩ԀƆ snoᴉɹɐʌ sʇɹoddns oslɐ ʇᴉ ʇnq 'pSqǝǝɹℲ puɐ 'SOɔɐɯ 'xnuᴉ˥ 'sʍopuᴉM ǝʞᴉl sɯǝʇsʎs ƃuᴉʇɐɹǝdo ʇuǝɹǝɟɟᴉp uo unɹ ʇᴉ sǝop ʎluo ʇoN ˙sǝᴉʇᴉlᴉqɐdɐɔ ɯɹoɟʇɐld-ssoɹɔ ƃuᴉzɐɯɐ sɐɥ ˥ƆWH
|
||||
|
||||
˙ǝɹoɯ puɐ 'uoᴉʇɐzᴉɯoʇsnƆ I∩ 'ƃuᴉʇɐǝɹƆ ʞɔɐdpoW '(ǝuᴉℲᴉʇdO puɐ 'ɹǝpɐo˥ǝʇᴉ˥ 'ʇlᴉnQ 'ɔᴉɹqɐℲ 'ɯooɹuɐǝlƆ 'ǝƃɹoℲoǝN 'ǝƃɹoℲ) ƃuᴉllɐʇsuI ɹǝpɐo˥poW 'ƃuᴉzᴉɯoʇsnƆ ǝɯɐפ 'ʇuǝɯǝƃɐuɐW poW sʇɹoddns ʇɐɥʇ ɹǝɥɔunɐl ʇɟɐɹɔǝuᴉW ɯɹoɟʇɐld-ssoɹɔ 'ǝɔɹnos-uǝdo uɐ sᴉ ˥ƆWH
|
||||
˙ǝɹoɯ puɐ 'uoᴉʇɐzᴉɯoʇsnƆ I∩ 'ƃuᴉʇɐǝɹƆ ʞɔɐdpoW '(ǝuᴉℲᴉʇdO puɐ 'ɹǝpɐo˥ǝʇᴉ˥ 'ʇlᴉnQ 'ɔᴉɹqɐℲ ʎɔɐᵷǝꞀ 'ɔᴉɹqɐℲ 'ɯooɹuɐǝlƆ 'ǝƃɹoℲoǝN 'ǝƃɹoℲ) ƃuᴉllɐʇsuI ɹǝpɐo˥poW 'ƃuᴉzᴉɯoʇsnƆ ǝɯɐפ 'ʇuǝɯǝƃɐuɐW poW sʇɹoddns ʇɐɥʇ ɹǝɥɔunɐl ʇɟɐɹɔǝuᴉW ɯɹoɟʇɐld-ssoɹɔ 'ǝɔɹnos-uǝdo uɐ sᴉ ˥ƆWH
|
||||
|
||||
## pɐoluʍoᗡ
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中
|
||||
|
||||
## Introducción
|
||||
|
||||
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
|
||||
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
|
||||
|
||||
HMCL tiene increíbles capacidades multiplataforma. No solo funciona en diferentes sistemas operativos como Windows, Linux, macOS y FreeBSD, sino que también es compatible con varias arquitecturas de CPU como x86, ARM, RISC-V, MIPS y LoongArch. Puedes disfrutar fácilmente de Minecraft en diferentes plataformas a través de HMCL.
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中
|
||||
|
||||
## 紹介
|
||||
|
||||
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
|
||||
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
|
||||
|
||||
HMCLは優れたクロスプラットフォーム性能を持っています。Windows、Linux、macOS、FreeBSDなどの異なるオペレーティングシステムだけでなく、x86、ARM、RISC-V、MIPS、LoongArchなどのさまざまなCPUアーキテクチャにも対応しています。HMCLを使えば、さまざまなプラットフォームでMinecraftを簡単に楽しむことができます。
|
||||
|
||||
|
||||
+14
-15
@@ -28,18 +28,18 @@
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
### 概說
|
||||
### 概敘
|
||||
|
||||
HMCL 者,開源之礦藝啟者也。能理改囊,善於遊戲之自定,且自動安裝諸如 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦可製作改囊集,界面亦可隨意更易。
|
||||
HMCL 者,開源之礦藝啟器也。善理改囊,可自定遊戲,自動裝設 Forge、NeoForge、Cleanroom、Fabric、Legacy Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦能制改囊集,界面隨心易之。
|
||||
|
||||
HMCL 跨域甚廣。無論 Windows、Linux、macOS、FreeBSD 諸常見械綱,抑或 x86、ARM、RISC-V、MIPS、LoongArch 諸大構處理器,皆可運行。
|
||||
君可憑此,於諸算機間自如遊戲。
|
||||
HMCL 跨域廣矣。Windows、Linux、macOS、FreeBSD 諸常見械綱,x86、ARM、RISC-V、MIPS、LoongArch 諸算器架構,皆可運行。
|
||||
憑此器,可遊《礦藝》於諸機之間,無礙也。
|
||||
|
||||
若欲詳知 HMCL 於諸算機之支援,請觀[此表](PLATFORM_zh_Hant.md)。
|
||||
欲知 HMCL 所支平臺,詳觀[此表](PLATFORM_zh_Hant.md)。
|
||||
|
||||
## 下載
|
||||
|
||||
君可自此數途引 HMCL:
|
||||
可從下述諸途得 HMCL:
|
||||
|
||||
- [HMCL 官方網站](https://hmcl.huangyuhui.net/download)
|
||||
- [GitHub Release](https://github.com/HMCL-dev/HMCL/releases)
|
||||
@@ -47,25 +47,24 @@ HMCL 跨域甚廣。無論 Windows、Linux、macOS、FreeBSD 諸常見械綱,
|
||||
|
||||
## 與貢
|
||||
|
||||
HMCL 為社群所驅之開源計畫,迎諸君貢獻其碼,或建言於此。
|
||||
HMCL 乃社群共驅之開源項目,迎諸君獻碼或建言。
|
||||
|
||||
君可由此數途參與 HMCL 之開發:
|
||||
參與之道如下:
|
||||
|
||||
- 於 GitHub 上[創建 Issue](https://github.com/HMCL-dev/HMCL/issues/new/choose),以報告謬誤,或請求功能。
|
||||
- 於 GitHub 上 Fork 倉庫,並[提交 Pull Request](https://github.com/HMCL-dev/HMCL/compare),以貢獻其碼。
|
||||
- 於 GitHub 上[創建 Issue](https://github.com/HMCL-dev/HMCL/issues/new/choose),以報謬誤或求功能。
|
||||
- 於 GitHub 上 Fork 倉庫,並[提交 Pull Request](https://github.com/HMCL-dev/HMCL/compare),以貢代碼。
|
||||
|
||||
參與貢獻之前,請閱[貢獻指南](./Contributing_zh.md),其中載有如下:
|
||||
貢獻之前,請閱[貢獻指南](./Contributing_zh_Hant.md),中含:
|
||||
|
||||
- [如何自源碼構建並運行 HMCL](./Contributing_zh.md#构建-hmcl)
|
||||
- [以調試選項調整 HMCL 之行為](./Contributing_zh.md#调试选项)
|
||||
- [如何自源碼構建並執行 HMCL](./Contributing_zh_Hant.md#構建-hmcl)
|
||||
- [以勘誤選項調整 HMCL 之行為](./Contributing_zh_Hant.md#除錯選項)
|
||||
|
||||
## 貢獻者
|
||||
|
||||
自公元二〇一五年以來,參與 HMCL 者已逾百十,謝其勤勞!
|
||||
自乙未年(2015)始,百十余人參與 HMCL,謝其勤勞!
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/graphs/contributors)
|
||||
|
||||
## 開源之約
|
||||
|
||||
詳見 [README_zh_Hant.md](README_zh_Hant.md#開源協議)。
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user