mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-15 06:45:42 -04:00
feat: support change skin of offline accounts.
This commit is contained in:
parent
01893b053d
commit
cd030c1de0
@ -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;
|
||||
|
@ -17,13 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.offline;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@ -51,13 +52,13 @@ public class OfflineAccount extends Account {
|
||||
private final AuthlibInjectorArtifactProvider downloader;
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private final Map<TextureType, Texture> textures;
|
||||
private Skin skin;
|
||||
|
||||
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Map<TextureType, Texture> textures) {
|
||||
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Skin skin) {
|
||||
this.downloader = requireNonNull(downloader);
|
||||
this.username = requireNonNull(username);
|
||||
this.uuid = requireNonNull(uuid);
|
||||
this.textures = textures;
|
||||
this.skin = skin;
|
||||
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be blank");
|
||||
@ -79,11 +80,20 @@ public class OfflineAccount extends Account {
|
||||
return username;
|
||||
}
|
||||
|
||||
public Skin getSkin() {
|
||||
return skin;
|
||||
}
|
||||
|
||||
public void setSkin(Skin skin) {
|
||||
this.skin = skin;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||
|
||||
if (skin != null || cape != null) {
|
||||
if (skin != null) {
|
||||
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return downloader.getArtifactInfo();
|
||||
@ -109,18 +119,14 @@ public class OfflineAccount extends Account {
|
||||
try {
|
||||
YggdrasilServer server = new YggdrasilServer(0);
|
||||
server.start();
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, TextureModel.STEVE,
|
||||
mapOf(
|
||||
pair(TextureType.SKIN, server.loadTexture(skin)),
|
||||
pair(TextureType.CAPE, server.loadTexture(cape))
|
||||
)));
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
|
||||
|
||||
return authInfo.withArguments(new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
|
||||
"-Dauthlibinjector.side=client"
|
||||
))
|
||||
.withCloseable(server::stop);
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
} else {
|
||||
@ -143,18 +149,20 @@ public class OfflineAccount extends Account {
|
||||
return mapOf(
|
||||
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
|
||||
pair("username", username),
|
||||
pair("skin", skin),
|
||||
pair("cape", cape)
|
||||
pair("skin", skin.toStorage())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
|
||||
return super.getTextures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("username", username)
|
||||
.append("uuid", uuid)
|
||||
.append("skin", skin)
|
||||
.append("cape", cape)
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
@ -45,25 +45,23 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
|
||||
}
|
||||
|
||||
public OfflineAccount create(String username, UUID uuid) {
|
||||
return new OfflineAccount(downloader, username, uuid, null, null);
|
||||
return new OfflineAccount(downloader, username, uuid, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) {
|
||||
AdditionalData data;
|
||||
UUID uuid;
|
||||
String skin;
|
||||
String cape;
|
||||
Skin skin;
|
||||
if (additionalData != null) {
|
||||
data = (AdditionalData) additionalData;
|
||||
uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid;
|
||||
skin = data.skin;
|
||||
cape = data.cape;
|
||||
} else {
|
||||
uuid = getUUIDFromUserName(username);
|
||||
skin = cape = null;
|
||||
skin = null;
|
||||
}
|
||||
return new OfflineAccount(downloader, username, uuid, skin, cape);
|
||||
return new OfflineAccount(downloader, username, uuid, skin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -73,10 +71,9 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
|
||||
UUID uuid = tryCast(storage.get("uuid"), String.class)
|
||||
.map(UUIDTypeAdapter::fromString)
|
||||
.orElse(getUUIDFromUserName(username));
|
||||
String skin = tryCast(storage.get("skin"), String.class).orElse(null);
|
||||
String cape = tryCast(storage.get("cape"), String.class).orElse(null);
|
||||
Skin skin = Skin.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null));
|
||||
|
||||
return new OfflineAccount(downloader, username, uuid, skin, cape);
|
||||
return new OfflineAccount(downloader, username, uuid, skin);
|
||||
}
|
||||
|
||||
public static UUID getUUIDFromUserName(String username) {
|
||||
@ -85,13 +82,11 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
|
||||
|
||||
public static class AdditionalData {
|
||||
private final UUID uuid;
|
||||
private final String skin;
|
||||
private final String cape;
|
||||
private final Skin skin;
|
||||
|
||||
public AdditionalData(UUID uuid, String skin, String cape) {
|
||||
public AdditionalData(UUID uuid, Skin skin) {
|
||||
this.uuid = uuid;
|
||||
this.skin = skin;
|
||||
this.cape = cape;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,26 +18,31 @@
|
||||
package org.jackhuang.hmcl.auth.offline;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class Skin {
|
||||
|
||||
@ -46,33 +51,81 @@ public class Skin {
|
||||
STEVE,
|
||||
ALEX,
|
||||
LOCAL_FILE,
|
||||
LITTLE_SKIN,
|
||||
CUSTOM_SKIN_LOADER_API,
|
||||
YGGDRASIL_API
|
||||
YGGDRASIL_API;
|
||||
|
||||
public static Type fromStorage(String type) {
|
||||
switch (type) {
|
||||
case "default":
|
||||
return DEFAULT;
|
||||
case "steve":
|
||||
return STEVE;
|
||||
case "alex":
|
||||
return ALEX;
|
||||
case "local_file":
|
||||
return LOCAL_FILE;
|
||||
case "little_skin":
|
||||
return LITTLE_SKIN;
|
||||
case "custom_skin_loader_api":
|
||||
return CUSTOM_SKIN_LOADER_API;
|
||||
case "yggdrasil_api":
|
||||
return YGGDRASIL_API;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Type type;
|
||||
private String value;
|
||||
private final Type type;
|
||||
private final String cslApi;
|
||||
private final String localSkinPath;
|
||||
private final String localCapePath;
|
||||
|
||||
public Skin(Type type, String cslApi, String localSkinPath, String localCapePath) {
|
||||
this.type = type;
|
||||
this.cslApi = cslApi;
|
||||
this.localSkinPath = localSkinPath;
|
||||
this.localCapePath = localCapePath;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
public String getCslApi() {
|
||||
return cslApi;
|
||||
}
|
||||
|
||||
public Task<Texture> toTexture(String username) {
|
||||
public String getLocalSkinPath() {
|
||||
return localSkinPath;
|
||||
}
|
||||
|
||||
public String getLocalCapePath() {
|
||||
return localCapePath;
|
||||
}
|
||||
|
||||
public Task<LoadedSkin> load(String username) {
|
||||
switch (type) {
|
||||
case DEFAULT:
|
||||
return Task.supplyAsync(() -> null);
|
||||
case STEVE:
|
||||
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")));
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
|
||||
case ALEX:
|
||||
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")));
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null));
|
||||
case LOCAL_FILE:
|
||||
return Task.supplyAsync(() -> Texture.loadTexture(Files.newInputStream(Paths.get(value))));
|
||||
return Task.supplyAsync(() -> {
|
||||
Texture skin = null, cape = null;
|
||||
Optional<Path> skinPath = FileUtils.tryGetPath(localSkinPath);
|
||||
Optional<Path> capePath = FileUtils.tryGetPath(localCapePath);
|
||||
if (skinPath.isPresent()) skin = Texture.loadTexture(Files.newInputStream(skinPath.get()));
|
||||
if (capePath.isPresent()) cape = Texture.loadTexture(Files.newInputStream(capePath.get()));
|
||||
return new LoadedSkin(TextureModel.STEVE, skin, cape);
|
||||
});
|
||||
case LITTLE_SKIN:
|
||||
case CUSTOM_SKIN_LOADER_API:
|
||||
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", value, username))))
|
||||
String realCslApi = type == Type.LITTLE_SKIN ? "http://mcskin.littleservice.cn" : cslApi;
|
||||
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", realCslApi, username))))
|
||||
.thenComposeAsync(json -> {
|
||||
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
|
||||
|
||||
@ -80,13 +133,57 @@ public class Skin {
|
||||
return Task.supplyAsync(() -> null);
|
||||
}
|
||||
|
||||
return new FetchBytesTask(new URL(String.format("%s/textures/%s", value, result.getHash())), 3);
|
||||
}).thenApplyAsync(Texture::loadTexture);
|
||||
return Task.allOf(
|
||||
Task.supplyAsync(result::getModel),
|
||||
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
|
||||
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
|
||||
);
|
||||
}).thenApplyAsync(result -> {
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture skin, cape;
|
||||
if (result.get(1) != null) {
|
||||
skin = Texture.loadTexture((InputStream) result.get(1));
|
||||
} else {
|
||||
skin = null;
|
||||
}
|
||||
|
||||
if (result.get(2) != null) {
|
||||
cape = Texture.loadTexture((InputStream) result.get(2));
|
||||
} else {
|
||||
cape = null;
|
||||
}
|
||||
|
||||
return new LoadedSkin((TextureModel) result.get(0), skin, cape);
|
||||
});
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<?, ?> toStorage() {
|
||||
return mapOf(
|
||||
pair("type", type.name().toLowerCase(Locale.ROOT)),
|
||||
pair("cslApi", cslApi),
|
||||
pair("localSkinPath", localSkinPath),
|
||||
pair("localCapePath", localCapePath)
|
||||
);
|
||||
}
|
||||
|
||||
public static Skin fromStorage(Map<?, ?> storage) {
|
||||
if (storage == null) return null;
|
||||
|
||||
Type type = tryCast(storage.get("type"), String.class).flatMap(t -> Optional.ofNullable(Type.fromStorage(t)))
|
||||
.orElse(Type.DEFAULT);
|
||||
String cslApi = tryCast(storage.get("cslApi"), String.class).orElse(null);
|
||||
String localSkinPath = tryCast(storage.get("localSkinPath"), String.class).orElse(null);
|
||||
String localCapePath = tryCast(storage.get("localCapePath"), String.class).orElse(null);
|
||||
|
||||
return new Skin(type, cslApi, localSkinPath, localCapePath);
|
||||
}
|
||||
|
||||
private static class FetchBytesTask extends FetchTask<InputStream> {
|
||||
|
||||
public FetchBytesTask(URL url, int retry) {
|
||||
@ -127,6 +224,30 @@ public class Skin {
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadedSkin {
|
||||
private final TextureModel model;
|
||||
private final Texture skin;
|
||||
private final Texture cape;
|
||||
|
||||
public LoadedSkin(TextureModel model, Texture skin, Texture cape) {
|
||||
this.model = model;
|
||||
this.skin = skin;
|
||||
this.cape = cape;
|
||||
}
|
||||
|
||||
public TextureModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public Texture getSkin() {
|
||||
return skin;
|
||||
}
|
||||
|
||||
public Texture getCape() {
|
||||
return cape;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SkinJson {
|
||||
private final String username;
|
||||
private final String skin;
|
||||
@ -183,6 +304,12 @@ public class Skin {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getCapeHash() {
|
||||
if (textures != null && textures.cape != null) {
|
||||
return textures.cape;
|
||||
} else return cape;
|
||||
}
|
||||
|
||||
public static class TextureJson {
|
||||
@SerializedName("default")
|
||||
private final String defaultSkin;
|
||||
|
@ -42,6 +42,10 @@ public class Texture {
|
||||
this.data = requireNonNull(data);
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ package org.jackhuang.hmcl.auth.offline;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.util.KeyUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@ -36,7 +34,6 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@ -143,14 +140,12 @@ public class YggdrasilServer extends HttpServer {
|
||||
public static class Character {
|
||||
private final UUID uuid;
|
||||
private final String name;
|
||||
private final TextureModel model;
|
||||
private final Map<TextureType, Texture> textures;
|
||||
private final Skin.LoadedSkin skin;
|
||||
|
||||
public Character(UUID uuid, String name, TextureModel model, Map<TextureType, Texture> textures) {
|
||||
public Character(UUID uuid, String name, Skin.LoadedSkin skin) {
|
||||
this.uuid = uuid;
|
||||
this.name = name;
|
||||
this.model = model;
|
||||
this.textures = textures;
|
||||
this.skin = Objects.requireNonNull(skin);
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
@ -161,30 +156,17 @@ public class YggdrasilServer extends HttpServer {
|
||||
return name;
|
||||
}
|
||||
|
||||
public TextureModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public Map<TextureType, Texture> getTextures() {
|
||||
return textures;
|
||||
}
|
||||
|
||||
private Map<String, Object> createKeyValue(String key, String value) {
|
||||
return mapOf(
|
||||
pair("name", key),
|
||||
pair("value", value)
|
||||
);
|
||||
}
|
||||
|
||||
public GameProfile toSimpleResponse() {
|
||||
return new GameProfile(uuid, name);
|
||||
}
|
||||
|
||||
public Object toCompleteResponse(String rootUrl) {
|
||||
Map<String, Object> realTextures = new HashMap<>();
|
||||
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) {
|
||||
if (textureEntry.getValue() == null) continue;
|
||||
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().getHash())));
|
||||
if (skin.getSkin() != null) {
|
||||
realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
|
||||
}
|
||||
if (skin.getCape() != null) {
|
||||
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
|
||||
}
|
||||
|
||||
Map<String, Object> textureResponse = mapOf(
|
||||
|
@ -340,7 +340,7 @@ public abstract class Task<T> {
|
||||
messageUpdate.accept(newMessage);
|
||||
}
|
||||
|
||||
public final void run() throws Exception {
|
||||
public final T run() throws Exception {
|
||||
if (getSignificance().shouldLog())
|
||||
Logging.LOG.log(Level.FINE, "Executing task: " + getName());
|
||||
|
||||
@ -350,6 +350,8 @@ public abstract class Task<T> {
|
||||
for (Task<?> task : getDependencies())
|
||||
doSubTask(task);
|
||||
onDone.fireEvent(new TaskEvent(this, this, false));
|
||||
|
||||
return getResult();
|
||||
}
|
||||
|
||||
private void doSubTask(Task<?> task) throws Exception {
|
||||
|
@ -413,6 +413,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static Optional<Path> tryGetPath(String first, String... more) {
|
||||
if (first == null) return Optional.empty();
|
||||
try {
|
||||
return Optional.of(Paths.get(first, more));
|
||||
} catch (InvalidPathException e) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user