feat: support change skin of offline accounts.
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.PerspectiveCamera;
|
||||
import javafx.scene.SceneAntialiasing;
|
||||
import javafx.scene.SubScene;
|
||||
import javafx.scene.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Shape3D;
|
||||
@@ -16,9 +13,8 @@ import javafx.scene.transform.Translate;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/steve.png"));
|
||||
public static final Image CHOCOLATE = new Image(SkinCanvas.class.getResourceAsStream("/chocolate.png"));
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img//steve.png"));
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
@@ -208,7 +204,6 @@ public class SkinCanvas extends Group {
|
||||
|
||||
subScene = new SubScene(group, preW, preH, true,
|
||||
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
|
||||
subScene.setFill(Color.ALICEBLUE);
|
||||
subScene.setCamera(camera);
|
||||
|
||||
return subScene;
|
||||
@@ -218,4 +213,45 @@ public class SkinCanvas extends Group {
|
||||
getChildren().add(createSubScene());
|
||||
}
|
||||
|
||||
private double lastX, lastY;
|
||||
|
||||
public void enableRotation(double sensitivity) {
|
||||
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
lastX = -1;
|
||||
lastY = -1;
|
||||
});
|
||||
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
if (!(lastX == -1 || lastY == -1)) {
|
||||
if (e.isAltDown() || e.isControlDown() || e.isShiftDown()) {
|
||||
if (e.isShiftDown())
|
||||
zRotate.setAngle(zRotate.getAngle() - (e.getSceneY() - lastY) * sensitivity);
|
||||
if (e.isAltDown())
|
||||
yRotate.setAngle(yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity);
|
||||
if (e.isControlDown())
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity);
|
||||
} else {
|
||||
double yaw = yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity;
|
||||
yaw %= 360;
|
||||
if (yaw < 0)
|
||||
yaw += 360;
|
||||
|
||||
int flagX = yaw < 90 || yaw > 270 ? 1 : -1;
|
||||
int flagZ = yaw < 180 ? -1 : 1;
|
||||
double kx = Math.abs(90 - yaw % 180) / 90 * flagX, kz = Math.abs(90 - (yaw + 90) % 180) / 90 * flagZ;
|
||||
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kx);
|
||||
yRotate.setAngle(yaw);
|
||||
zRotate.setAngle(zRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kz);
|
||||
}
|
||||
}
|
||||
lastX = e.getSceneX();
|
||||
lastY = e.getSceneY();
|
||||
});
|
||||
addEventHandler(ScrollEvent.SCROLL, e -> {
|
||||
double delta = (e.getDeltaY() > 0 ? 1 : e.getDeltaY() == 0 ? 0 : -1) / 10D * sensitivity;
|
||||
scale.setX(Math.min(Math.max(scale.getX() - delta, 0.1), 10));
|
||||
scale.setY(Math.min(Math.max(scale.getY() - delta, 0.1), 10));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
@@ -46,6 +49,7 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -54,8 +58,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -135,6 +137,8 @@ public class AccountListItem extends RadioButton {
|
||||
} else {
|
||||
return createBooleanBinding(() -> true);
|
||||
}
|
||||
} else if (account instanceof OfflineAccount) {
|
||||
return createBooleanBinding(() -> true);
|
||||
} else {
|
||||
return createBooleanBinding(() -> false);
|
||||
}
|
||||
@@ -145,6 +149,10 @@ public class AccountListItem extends RadioButton {
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (account instanceof OfflineAccount) {
|
||||
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
|
||||
return null;
|
||||
}
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -523,7 +523,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
return getAuthServer();
|
||||
} else if (factory instanceof OfflineAccountFactory) {
|
||||
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
|
||||
return new OfflineAccountFactory.AdditionalData(uuid, null, null);
|
||||
return new OfflineAccountFactory.AdditionalData(uuid, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,19 +17,184 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class OfflineAccountSkinPane extends StackPane {
|
||||
private final OfflineAccount account;
|
||||
|
||||
private final MultiFileItem<Skin.Type> skinItem = new MultiFileItem<>();
|
||||
private final JFXTextField cslApiField = new JFXTextField();
|
||||
private final FileSelector skinSelector = new FileSelector();
|
||||
private final FileSelector capeSelector = new FileSelector();
|
||||
|
||||
private final InvalidationListener skinBinding;
|
||||
|
||||
public OfflineAccountSkinPane(OfflineAccount account) {
|
||||
this.account = account;
|
||||
|
||||
getStyleClass().add("skin-pane");
|
||||
|
||||
JFXDialogLayout layout = new JFXDialogLayout();
|
||||
getChildren().setAll(layout);
|
||||
layout.setHeading(new Label(i18n("account.skin")));
|
||||
|
||||
MultiFileItem<>
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
|
||||
StackPane canvasPane = new StackPane(canvas);
|
||||
canvasPane.setPrefWidth(300);
|
||||
canvasPane.setPrefHeight(300);
|
||||
pane.setCenter(canvas);
|
||||
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
|
||||
canvas.enableRotation(.5);
|
||||
|
||||
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
|
||||
if (e.getDragboard().hasFiles()) {
|
||||
File file = e.getDragboard().getFiles().get(0);
|
||||
if (file.getAbsolutePath().endsWith(".png"))
|
||||
e.acceptTransferModes(TransferMode.COPY);
|
||||
}
|
||||
});
|
||||
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
|
||||
if (e.isAccepted()) {
|
||||
File skin = e.getDragboard().getFiles().get(0);
|
||||
Platform.runLater(() -> {
|
||||
skinSelector.setValue(skin.getAbsolutePath());
|
||||
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
TransitionPane skinOptionPane = new TransitionPane();
|
||||
skinOptionPane.setMaxWidth(300);
|
||||
VBox optionPane = new VBox(skinItem, skinOptionPane);
|
||||
pane.setRight(optionPane);
|
||||
|
||||
layout.setBody(pane);
|
||||
|
||||
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
|
||||
cslApiField.setValidators(new URLValidator());
|
||||
|
||||
skinItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
|
||||
new MultiFileItem.Option<>("Steve", Skin.Type.STEVE),
|
||||
new MultiFileItem.Option<>("Alex", Skin.Type.ALEX),
|
||||
new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE),
|
||||
new MultiFileItem.Option<>("LittleSkin", Skin.Type.LITTLE_SKIN),
|
||||
new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API)
|
||||
));
|
||||
|
||||
if (account.getSkin() == null) {
|
||||
skinItem.setSelectedData(Skin.Type.DEFAULT);
|
||||
} else {
|
||||
skinItem.setSelectedData(account.getSkin().getType());
|
||||
cslApiField.setText(account.getSkin().getCslApi());
|
||||
skinSelector.setValue(account.getSkin().getLocalSkinPath());
|
||||
capeSelector.setValue(account.getSkin().getLocalCapePath());
|
||||
}
|
||||
|
||||
skinBinding = FXUtils.observeWeak(() -> {
|
||||
getSkin().load(account.getUsername())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to load skin", exception);
|
||||
Controllers.showToast(i18n("message.failed"));
|
||||
} else {
|
||||
if (result == null || result.getSkin() == null) {
|
||||
canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
|
||||
}
|
||||
}).start();
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(0, 0, 0, 10));
|
||||
gridPane.setHgap(16);
|
||||
gridPane.setVgap(8);
|
||||
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
|
||||
switch (selectedData) {
|
||||
case DEFAULT:
|
||||
case STEVE:
|
||||
case ALEX:
|
||||
case LITTLE_SKIN:
|
||||
break;
|
||||
case LOCAL_FILE:
|
||||
gridPane.addRow(0, new Label(i18n("account.skin")), skinSelector);
|
||||
gridPane.addRow(1, new Label(i18n("account.cape")), capeSelector);
|
||||
break;
|
||||
case CUSTOM_SKIN_LOADER_API:
|
||||
gridPane.addRow(0, new Label(i18n("account.skin.type.csl_api.location")), cslApiField);
|
||||
break;
|
||||
}
|
||||
|
||||
skinOptionPane.setContent(gridPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
});
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
acceptButton.setOnAction(e -> {
|
||||
account.setSkin(getSkin());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
|
||||
JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin.hint"));
|
||||
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://mcskin.littleservice.cn/"));
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
layout.setActions(littleSkinLink, acceptButton, cancelButton);
|
||||
}
|
||||
|
||||
private Skin getSkin() {
|
||||
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), skinSelector.getValue(), capeSelector.getValue());
|
||||
}
|
||||
|
||||
private boolean isDefaultSlim() {
|
||||
return TextureModel.detectUUID(account.getUUID()) == TextureModel.ALEX;
|
||||
}
|
||||
|
||||
private Image getDefaultTexture() {
|
||||
if (isDefaultSlim()) {
|
||||
return SkinCanvas.ALEX;
|
||||
} else {
|
||||
return SkinCanvas.STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
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.layout.HBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FileSelector extends HBox {
|
||||
private StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
|
||||
public String getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
public StringProperty valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
public String getChooserTitle() {
|
||||
return chooserTitle;
|
||||
}
|
||||
|
||||
public FileSelector setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle = chooserTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public FileSelector setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
return extensionFilters;
|
||||
}
|
||||
|
||||
public FileSelector() {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
customField.textProperty().bindBidirectional(valueProperty());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
selectButton.setOnMouseClicked(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
if (dir != null)
|
||||
customField.setText(dir.getAbsolutePath());
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
customField.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(3);
|
||||
getChildren().addAll(customField, selectButton);
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -31,39 +30,28 @@ import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiFileItem<T> extends ComponentSublist {
|
||||
public class MultiFileItem<T> extends VBox {
|
||||
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
|
||||
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
|
||||
|
||||
private final ToggleGroup group = new ToggleGroup();
|
||||
private final VBox pane = new VBox();
|
||||
|
||||
private Consumer<Toggle> toggleSelectedListener;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public MultiFileItem() {
|
||||
pane.setStyle("-fx-padding: 0 0 10 0;");
|
||||
pane.setSpacing(8);
|
||||
|
||||
getContent().add(pane);
|
||||
setPadding(new Insets(0, 0, 10, 0));
|
||||
setSpacing(8);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
if (toggleSelectedListener != null)
|
||||
@@ -86,7 +74,7 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
}
|
||||
|
||||
public void loadChildren(Collection<Option<T>> options) {
|
||||
pane.getChildren().setAll(options.stream()
|
||||
getChildren().setAll(options.stream()
|
||||
.map(option -> option.createItem(group))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
@@ -183,6 +171,7 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
|
||||
public static class StringOption<T> extends Option<T> {
|
||||
private StringProperty value = new SimpleStringProperty();
|
||||
private ValidatorBase[] validators;
|
||||
|
||||
public StringOption(String title, T data) {
|
||||
super(title, data);
|
||||
@@ -205,6 +194,11 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringOption<T> setValidators(ValidatorBase... validators) {
|
||||
this.validators = validators;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
@@ -221,6 +215,9 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
||||
customField.textProperty().bindBidirectional(valueProperty());
|
||||
customField.disableProperty().bind(left.selectedProperty().not());
|
||||
if (validators != null) {
|
||||
customField.setValidators(validators);
|
||||
}
|
||||
pane.setRight(customField);
|
||||
|
||||
return pane;
|
||||
@@ -228,44 +225,41 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
}
|
||||
|
||||
public static class FileOption<T> extends Option<T> {
|
||||
private StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
private FileSelector selector = new FileSelector();
|
||||
|
||||
public FileOption(String title, T data) {
|
||||
super(title, data);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value.get();
|
||||
return selector.getValue();
|
||||
}
|
||||
|
||||
public StringProperty valueProperty() {
|
||||
return value;
|
||||
return selector.valueProperty();
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value.set(value);
|
||||
selector.setValue(value);
|
||||
}
|
||||
|
||||
public FileOption<T> setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
selector.setDirectory(directory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FileOption<T> bindBidirectional(Property<String> property) {
|
||||
this.value.bindBidirectional(property);
|
||||
selector.valueProperty().bindBidirectional(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FileOption<T> setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle = chooserTitle;
|
||||
selector.setChooserTitle(chooserTitle);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
return extensionFilters;
|
||||
return selector.getExtensionFilters();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -280,36 +274,9 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
JFXTextField customField = new JFXTextField();
|
||||
customField.textProperty().bindBidirectional(valueProperty());
|
||||
customField.disableProperty().bind(left.selectedProperty().not());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.disableProperty().bind(left.selectedProperty().not());
|
||||
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
selectButton.setOnMouseClicked(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
if (dir != null)
|
||||
customField.setText(dir.getAbsolutePath());
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
customField.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.setSpacing(3);
|
||||
right.getChildren().addAll(customField, selectButton);
|
||||
pane.setRight(right);
|
||||
selector.disableProperty().bind(left.selectedProperty().not());
|
||||
BorderPane.setAlignment(selector, Pos.CENTER_RIGHT);
|
||||
pane.setRight(selector);
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class URLValidator extends ValidatorBase {
|
||||
private final boolean nullable;
|
||||
|
||||
public URLValidator() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public URLValidator(@NamedArg("nullable") boolean nullable) {
|
||||
this(i18n("input.url"), nullable);
|
||||
}
|
||||
|
||||
public URLValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
|
||||
super(message);
|
||||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void eval() {
|
||||
if (srcControl.get() instanceof TextInputControl) {
|
||||
evalTextInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private void evalTextInputField() {
|
||||
TextInputControl textField = ((TextInputControl) srcControl.get());
|
||||
|
||||
if (StringUtils.isBlank(textField.getText()))
|
||||
hasErrors.set(!nullable);
|
||||
else {
|
||||
try {
|
||||
new URL(textField.getText()).toURI();
|
||||
hasErrors.set(false);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
hasErrors.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -243,17 +240,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new Validator(s -> {
|
||||
if (s.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(s).toURI();
|
||||
return true;
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
txtModpackFileApi.getValidators().add(new URLValidator());
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,11 +86,13 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
StackPane componentList = new StackPane();
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>();
|
||||
backgroundItem.setTitle(i18n("launcher.background"));
|
||||
backgroundItem.setHasSubtitle(true);
|
||||
ComponentSublist backgroundSublist = new ComponentSublist();
|
||||
backgroundSublist.getContent().add(backgroundItem);
|
||||
backgroundSublist.setTitle(i18n("launcher.background"));
|
||||
backgroundSublist.setHasSubtitle(true);
|
||||
|
||||
backgroundItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
|
||||
@@ -102,12 +104,12 @@ public class PersonalizationPage extends StackPane {
|
||||
.bindBidirectional(config().backgroundImageUrlProperty())
|
||||
));
|
||||
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
|
||||
backgroundItem.subtitleProperty().bind(
|
||||
backgroundSublist.subtitleProperty().bind(
|
||||
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
|
||||
.then(i18n("launcher.background.default"))
|
||||
.otherwise(config().backgroundImageProperty()));
|
||||
|
||||
componentList.getChildren().add(backgroundItem);
|
||||
componentList.getContent().add(backgroundItem);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class SettingsPage extends SettingsView {
|
||||
// ====
|
||||
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
fileCommonLocation.subtitleProperty().bind(
|
||||
fileCommonLocationSublist.subtitleProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
@@ -48,6 +48,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public abstract class SettingsView extends StackPane {
|
||||
protected final JFXComboBox<SupportedLocale> cboLanguage;
|
||||
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
|
||||
protected final ComponentSublist fileCommonLocationSublist;
|
||||
protected final Label lblUpdate;
|
||||
protected final Label lblUpdateSub;
|
||||
protected final JFXRadioButton chkUpdateStable;
|
||||
@@ -144,8 +145,10 @@ public abstract class SettingsView extends StackPane {
|
||||
|
||||
{
|
||||
fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocation.setHasSubtitle(true);
|
||||
fileCommonLocationSublist = new ComponentSublist();
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
@@ -159,10 +162,10 @@ public abstract class SettingsView extends StackPane {
|
||||
cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
|
||||
cleanButton.getStyleClass().add("jfx-button-border");
|
||||
|
||||
fileCommonLocation.setHeaderRight(cleanButton);
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
}
|
||||
|
||||
settingsPane.getContent().add(fileCommonLocation);
|
||||
settingsPane.getContent().add(fileCommonLocationSublist);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -99,10 +99,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final OptionToggleButton noJVMCheckPane;
|
||||
private final OptionToggleButton useNativeGLFWPane;
|
||||
private final OptionToggleButton useNativeOpenALPane;
|
||||
private final ComponentSublist javaSublist;
|
||||
private final MultiFileItem<JavaVersion> javaItem;
|
||||
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
|
||||
private final ComponentSublist gameDirSublist;
|
||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final JFXComboBox<ProcessPriority> cboProcessPriority;
|
||||
@@ -192,14 +195,18 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
componentList.setDepth(1);
|
||||
|
||||
javaItem = new MultiFileItem<>();
|
||||
javaItem.setTitle(i18n("settings.game.java_directory"));
|
||||
javaItem.setHasSubtitle(true);
|
||||
javaSublist = new ComponentSublist();
|
||||
javaSublist.getContent().add(javaItem);
|
||||
javaSublist.setTitle(i18n("settings.game.java_directory"));
|
||||
javaSublist.setHasSubtitle(true);
|
||||
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
|
||||
.setChooserTitle(i18n("settings.game.java_directory.choose"));
|
||||
|
||||
gameDirItem = new MultiFileItem<>();
|
||||
gameDirItem.setTitle(i18n("settings.game.working_directory"));
|
||||
gameDirItem.setHasSubtitle(true);
|
||||
gameDirSublist = new ComponentSublist();
|
||||
gameDirSublist.getContent().add(gameDirItem);
|
||||
gameDirSublist.setTitle(i18n("settings.game.working_directory"));
|
||||
gameDirSublist.setHasSubtitle(true);
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
@@ -395,7 +402,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
|
||||
componentList.getContent().setAll(javaItem, gameDirItem, maxMemoryPane, launcherVisibilityPane, dimensionPane, showLogsPane, processPriorityPane, serverPane);
|
||||
componentList.getContent().setAll(javaSublist, gameDirSublist, maxMemoryPane, launcherVisibilityPane, dimensionPane, showLogsPane, processPriorityPane, serverPane);
|
||||
}
|
||||
|
||||
HBox advancedHintPane = new HBox();
|
||||
@@ -474,8 +481,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
workaroundPane.disableProperty().bind(enableSpecificSettings.not());
|
||||
{
|
||||
nativesDirItem = new MultiFileItem<>();
|
||||
nativesDirItem.setTitle(i18n("settings.advanced.natives_directory"));
|
||||
nativesDirItem.setHasSubtitle(true);
|
||||
nativesDirSublist = new ComponentSublist();
|
||||
nativesDirSublist.getContent().add(nativesDirItem);
|
||||
nativesDirSublist.setTitle(i18n("settings.advanced.natives_directory"));
|
||||
nativesDirSublist.setHasSubtitle(true);
|
||||
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), NativesDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
|
||||
.setDirectory(true);
|
||||
@@ -499,7 +508,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
useNativeOpenALPane = new OptionToggleButton();
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
|
||||
workaroundPane.getContent().setAll(nativesDirItem, noJVMArgsPane, noGameCheckPane, noJVMCheckPane, useNativeGLFWPane, useNativeOpenALPane);
|
||||
workaroundPane.getContent().setAll(nativesDirSublist, noJVMArgsPane, noGameCheckPane, noJVMCheckPane, useNativeGLFWPane, useNativeOpenALPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().addAll(componentList,
|
||||
@@ -613,10 +622,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
lastVersionSetting.javaProperty().removeListener(javaListener);
|
||||
|
||||
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
|
||||
gameDirItem.subtitleProperty().unbind();
|
||||
gameDirSublist.subtitleProperty().unbind();
|
||||
|
||||
nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty());
|
||||
nativesDirItem.subtitleProperty().unbind();
|
||||
nativesDirSublist.subtitleProperty().unbind();
|
||||
}
|
||||
|
||||
// unbind data fields
|
||||
@@ -663,11 +672,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
versionSetting.javaProperty().addListener(javaListener);
|
||||
|
||||
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
|
||||
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
|
||||
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
|
||||
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
|
||||
|
||||
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
|
||||
nativesDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
|
||||
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
|
||||
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
|
||||
|
||||
lastVersionSetting = versionSetting;
|
||||
@@ -703,7 +712,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
if (versionSetting == null)
|
||||
return;
|
||||
Task.supplyAsync(versionSetting::getJavaVersion)
|
||||
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
|
||||
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
|
||||
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
|
||||
.start();
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
-fx-fill: #856404;
|
||||
}
|
||||
|
||||
.skin-pane .jfx-text-field {
|
||||
-fx-pref-width: 200;
|
||||
}
|
||||
|
||||
.memory-label {
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ about.open_source=Open Source
|
||||
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
|
||||
|
||||
account=Accounts
|
||||
account.cape=Cape
|
||||
account.character=character
|
||||
account.choose=Choose a character
|
||||
account.create=Create a new account
|
||||
@@ -110,7 +111,13 @@ account.missing=No Account
|
||||
account.missing.add=Click here to add
|
||||
account.not_logged_in=Not logged in
|
||||
account.password=Password
|
||||
account.skin.file=Skin file
|
||||
account.skin=Skin
|
||||
account.skin.file=Skin image file
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=Address
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI
|
||||
account.skin.type.little_skin.hint=LittleSkin
|
||||
account.skin.type.local_file=Local skin image file
|
||||
account.skin.upload=Upload skin
|
||||
account.skin.upload.failed=Failed to upload skin
|
||||
account.skin.invalid_skin=Unrecognized skin file
|
||||
@@ -459,6 +466,7 @@ main_page=Home
|
||||
message.cancelled=Operation was cancelled
|
||||
message.confirm=Confirm
|
||||
message.copied=Copied to clipboard
|
||||
message.default=Default
|
||||
message.doing=Please wait
|
||||
message.downloading=Downloading...
|
||||
message.error=Error
|
||||
|
||||
@@ -35,14 +35,14 @@ about.thanks_to=鳴謝
|
||||
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載源,請贊助支持 BMCLAPI!
|
||||
about.thanks_to.contributors=所有通過 Issues、Pull Requests 等管道參與本項目的貢獻者
|
||||
about.thanks_to.contributors.statement=沒有開源社區的支持,Hello Minecraft! Launcher 無法走到今天
|
||||
about.thanks_to.gamerteam.statement=提供默認背景圖
|
||||
about.thanks_to.gamerteam.statement=提供預設背景圖
|
||||
about.thanks_to.mcbbs=MCBBS 我的世界中文論壇
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源
|
||||
about.thanks_to.mcmod=MC 百科
|
||||
about.thanks_to.mcmod.statement=提供 Mod 中文名映射表與 Mod 百科
|
||||
about.thanks_to.noin=這裡 (noin.cn)
|
||||
about.thanks_to.noin.statement=提供多人聯機服務 (cato - ioi 系列作品)
|
||||
about.thanks_to.red_lnn.statement=提供默認背景圖
|
||||
about.thanks_to.red_lnn.statement=提供預設背景圖
|
||||
about.thanks_to.users=HMCL 用戶群成員
|
||||
about.thanks_to.users.statement=感謝用戶群成員贊助充電、積極催更、迴響問題、出謀劃策
|
||||
about.thanks_to.yushijinhun.statement=authlib-injector 相关支援
|
||||
@@ -50,6 +50,7 @@ about.open_source=開放原始碼
|
||||
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
|
||||
|
||||
account=帳戶
|
||||
account.cape=披風
|
||||
account.character=角色
|
||||
account.choose=請選擇角色
|
||||
account.create=建立帳戶
|
||||
@@ -110,7 +111,13 @@ account.missing=沒有遊戲帳戶
|
||||
account.missing.add=按一下此處加入帳戶
|
||||
account.not_logged_in=未登入
|
||||
account.password=密碼
|
||||
account.skin=皮膚
|
||||
account.skin.file=皮膚圖片檔案
|
||||
account.skin.type.csl_api=Blessing Skin 伺服器
|
||||
account.skin.type.csl_api.location=伺服器位址
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI 位址
|
||||
account.skin.type.little_skin.hint=LittleSkin 皮膚站
|
||||
account.skin.type.local_file=本地皮膚圖片檔案
|
||||
account.skin.upload=上傳皮膚
|
||||
account.skin.upload.failed=皮膚上傳失敗
|
||||
account.skin.invalid_skin=無法識別的皮膚檔案
|
||||
@@ -459,6 +466,7 @@ main_page=首頁
|
||||
message.cancelled=操作被取消
|
||||
message.confirm=提示
|
||||
message.copied=已複製到剪貼板
|
||||
message.default=預設
|
||||
message.doing=請耐心等待
|
||||
message.downloading=正在下載…
|
||||
message.error=錯誤
|
||||
@@ -708,14 +716,14 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
|
||||
settings.advanced.java_permanent_generation_space.prompt=格式: MB
|
||||
settings.advanced.jvm=Java 虛擬機設定
|
||||
settings.advanced.jvm_args=Java 虛擬機參數
|
||||
settings.advanced.jvm_args.prompt=填寫此處可以覆蓋默認設定
|
||||
settings.advanced.jvm_args.prompt=填寫此處可以覆蓋預設設定
|
||||
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
|
||||
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
|
||||
settings.advanced.launcher_visibility.keep=不隱藏啟動器
|
||||
settings.advanced.launcher_visible=啟動器可見性
|
||||
settings.advanced.minecraft_arguments=Minecraft 額外參數
|
||||
settings.advanced.minecraft_arguments.prompt=默認
|
||||
settings.advanced.minecraft_arguments.prompt=預設
|
||||
settings.advanced.natives_directory=本地庫路徑(LWJGL)
|
||||
settings.advanced.natives_directory.choose=選擇本地庫路徑
|
||||
settings.advanced.natives_directory.default=預設(.minecraft/versions/<版本名>/natives/)
|
||||
@@ -731,7 +739,7 @@ settings.advanced.process_priority.high=高(優先保證遊戲運行,但可
|
||||
settings.advanced.post_exit_command=遊戲結束後執行命令
|
||||
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
|
||||
settings.advanced.server_ip=伺服器位址
|
||||
settings.advanced.server_ip.prompt=默認,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.use_native_glfw=[Linux] 使用系統 GLFW
|
||||
settings.advanced.use_native_openal=[Linux] 使用系統 OpenAL
|
||||
settings.advanced.workaround=除錯選項
|
||||
|
||||
@@ -50,6 +50,7 @@ about.open_source=开源
|
||||
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
|
||||
|
||||
account=帐户
|
||||
account.cape=披风
|
||||
account.character=角色
|
||||
account.choose=选择一个角色
|
||||
account.create=添加帐户
|
||||
@@ -110,7 +111,13 @@ account.missing=没有游戏帐户
|
||||
account.missing.add=点击此处添加帐户
|
||||
account.not_logged_in=未登录
|
||||
account.password=密码
|
||||
account.skin=皮肤
|
||||
account.skin.file=皮肤图片文件
|
||||
account.skin.type.csl_api=Blessing Skin 服务器
|
||||
account.skin.type.csl_api.location=服务器地址
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI 地址
|
||||
account.skin.type.little_skin.hint=LittleSkin 皮肤站
|
||||
account.skin.type.local_file=本地皮肤图片文件
|
||||
account.skin.upload=上传皮肤
|
||||
account.skin.upload.failed=皮肤上传失败
|
||||
account.skin.invalid_skin=无法识别的皮肤文件
|
||||
@@ -459,6 +466,7 @@ main_page=主页
|
||||
message.cancelled=操作被取消
|
||||
message.confirm=提示
|
||||
message.copied=已复制到剪贴板
|
||||
message.default=默认
|
||||
message.doing=请耐心等待
|
||||
message.downloading=正在下载
|
||||
message.error=错误
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package moe.mickey.minecraft.skin.fx.test;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
@@ -11,12 +9,14 @@ import moe.mickey.minecraft.skin.fx.SkinCanvasSupport;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Test extends Application {
|
||||
|
||||
public static final String TITLE = "FX - Minecraft skin preview";
|
||||
|
||||
public static SkinCanvas createSkinCanvas() {
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.CHOCOLATE, 400, 400, true);
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 400, 400, true);
|
||||
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
|
||||
FunctionHelper.alwaysB(Consumer<SkinCanvas>::accept, canvas, new SkinCanvasSupport.Mouse(.5), new SkinCanvasSupport.Drag(TITLE));
|
||||
return canvas;
|
||||
|
||||
Reference in New Issue
Block a user