feat: support change skin of offline accounts.

This commit is contained in:
huanghongxun 2021-09-25 02:04:25 +08:00
parent 01893b053d
commit cd030c1de0
24 changed files with 672 additions and 192 deletions

View File

@ -1,12 +1,9 @@
package moe.mickey.minecraft.skin.fx; package moe.mickey.minecraft.skin.fx;
import javafx.scene.Group; import javafx.scene.*;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.image.Image; 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.Material;
import javafx.scene.paint.PhongMaterial; import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Shape3D; import javafx.scene.shape.Shape3D;
@ -16,9 +13,8 @@ import javafx.scene.transform.Translate;
public class SkinCanvas extends Group { public class SkinCanvas extends Group {
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/alex.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("/steve.png")); public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img//steve.png"));
public static final Image CHOCOLATE = new Image(SkinCanvas.class.getResourceAsStream("/chocolate.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_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); 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, subScene = new SubScene(group, preW, preH, true,
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED); msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
subScene.setFill(Color.ALICEBLUE);
subScene.setCamera(camera); subScene.setCamera(camera);
return subScene; return subScene;
@ -218,4 +213,45 @@ public class SkinCanvas extends Group {
getChildren().add(createSubScene()); 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));
});
}
} }

View File

@ -20,7 +20,10 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding; 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.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin; 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.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -54,8 +58,6 @@ import java.util.Set;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.logging.Level; import java.util.logging.Level;
import javax.imageio.ImageIO;
import static java.util.Collections.emptySet; import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding; import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Logging.LOG;
@ -135,6 +137,8 @@ public class AccountListItem extends RadioButton {
} else { } else {
return createBooleanBinding(() -> true); return createBooleanBinding(() -> true);
} }
} else if (account instanceof OfflineAccount) {
return createBooleanBinding(() -> true);
} else { } else {
return createBooleanBinding(() -> false); return createBooleanBinding(() -> false);
} }
@ -145,6 +149,10 @@ public class AccountListItem extends RadioButton {
*/ */
@Nullable @Nullable
public Task<?> uploadSkin() { public Task<?> uploadSkin() {
if (account instanceof OfflineAccount) {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (!(account instanceof YggdrasilAccount)) { if (!(account instanceof YggdrasilAccount)) {
return null; return null;
} }

View File

@ -523,7 +523,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
return getAuthServer(); return getAuthServer();
} else if (factory instanceof OfflineAccountFactory) { } else if (factory instanceof OfflineAccountFactory) {
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText()); 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 { } else {
return null; return null;
} }

View File

@ -17,19 +17,184 @@
*/ */
package org.jackhuang.hmcl.ui.account; package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout; 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.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 { 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) { public OfflineAccountSkinPane(OfflineAccount account) {
this.account = account;
getStyleClass().add("skin-pane");
JFXDialogLayout layout = new JFXDialogLayout(); JFXDialogLayout layout = new JFXDialogLayout();
getChildren().setAll(layout); 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;
}
}
} }

View File

@ -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);
}
}

View File

@ -17,11 +17,10 @@
*/ */
package org.jackhuang.hmcl.ui.construct; package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton; import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -31,39 +30,28 @@ import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser; 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.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class MultiFileItem<T> extends VBox {
public class MultiFileItem<T> extends ComponentSublist {
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData"); private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData"); private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
private final ToggleGroup group = new ToggleGroup(); private final ToggleGroup group = new ToggleGroup();
private final VBox pane = new VBox();
private Consumer<Toggle> toggleSelectedListener; private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public MultiFileItem() { public MultiFileItem() {
pane.setStyle("-fx-padding: 0 0 10 0;"); setPadding(new Insets(0, 0, 10, 0));
pane.setSpacing(8); setSpacing(8);
getContent().add(pane);
group.selectedToggleProperty().addListener((a, b, newValue) -> { group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null) if (toggleSelectedListener != null)
@ -86,7 +74,7 @@ public class MultiFileItem<T> extends ComponentSublist {
} }
public void loadChildren(Collection<Option<T>> options) { public void loadChildren(Collection<Option<T>> options) {
pane.getChildren().setAll(options.stream() getChildren().setAll(options.stream()
.map(option -> option.createItem(group)) .map(option -> option.createItem(group))
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
@ -183,6 +171,7 @@ public class MultiFileItem<T> extends ComponentSublist {
public static class StringOption<T> extends Option<T> { public static class StringOption<T> extends Option<T> {
private StringProperty value = new SimpleStringProperty(); private StringProperty value = new SimpleStringProperty();
private ValidatorBase[] validators;
public StringOption(String title, T data) { public StringOption(String title, T data) {
super(title, data); super(title, data);
@ -205,6 +194,11 @@ public class MultiFileItem<T> extends ComponentSublist {
return this; return this;
} }
public StringOption<T> setValidators(ValidatorBase... validators) {
this.validators = validators;
return this;
}
@Override @Override
protected Node createItem(ToggleGroup group) { protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane(); BorderPane pane = new BorderPane();
@ -221,6 +215,9 @@ public class MultiFileItem<T> extends ComponentSublist {
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT); BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
customField.textProperty().bindBidirectional(valueProperty()); customField.textProperty().bindBidirectional(valueProperty());
customField.disableProperty().bind(left.selectedProperty().not()); customField.disableProperty().bind(left.selectedProperty().not());
if (validators != null) {
customField.setValidators(validators);
}
pane.setRight(customField); pane.setRight(customField);
return pane; return pane;
@ -228,44 +225,41 @@ public class MultiFileItem<T> extends ComponentSublist {
} }
public static class FileOption<T> extends Option<T> { public static class FileOption<T> extends Option<T> {
private StringProperty value = new SimpleStringProperty(); private FileSelector selector = new FileSelector();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
public FileOption(String title, T data) { public FileOption(String title, T data) {
super(title, data); super(title, data);
} }
public String getValue() { public String getValue() {
return value.get(); return selector.getValue();
} }
public StringProperty valueProperty() { public StringProperty valueProperty() {
return value; return selector.valueProperty();
} }
public void setValue(String value) { public void setValue(String value) {
this.value.set(value); selector.setValue(value);
} }
public FileOption<T> setDirectory(boolean directory) { public FileOption<T> setDirectory(boolean directory) {
this.directory = directory; selector.setDirectory(directory);
return this; return this;
} }
public FileOption<T> bindBidirectional(Property<String> property) { public FileOption<T> bindBidirectional(Property<String> property) {
this.value.bindBidirectional(property); selector.valueProperty().bindBidirectional(property);
return this; return this;
} }
public FileOption<T> setChooserTitle(String chooserTitle) { public FileOption<T> setChooserTitle(String chooserTitle) {
this.chooserTitle = chooserTitle; selector.setChooserTitle(chooserTitle);
return this; return this;
} }
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() { public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters; return selector.getExtensionFilters();
} }
@Override @Override
@ -280,36 +274,9 @@ public class MultiFileItem<T> extends ComponentSublist {
left.setUserData(data); left.setUserData(data);
pane.setLeft(left); pane.setLeft(left);
JFXTextField customField = new JFXTextField(); selector.disableProperty().bind(left.selectedProperty().not());
customField.textProperty().bindBidirectional(valueProperty()); BorderPane.setAlignment(selector, Pos.CENTER_RIGHT);
customField.disableProperty().bind(left.selectedProperty().not()); pane.setRight(selector);
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);
return pane; return pane;
} }
} }

View File

@ -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);
}
}
}
}

View File

@ -46,9 +46,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean; 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 RequiredValidator());
} }
txtModpackFileApi.getValidators().add(new Validator(s -> { txtModpackFileApi.getValidators().add(new URLValidator());
if (s.isEmpty()) {
return true;
}
try {
new URL(s).toURI();
return true;
} catch (IOException | URISyntaxException e) {
return false;
}
}));
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi); pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
} }

View File

@ -86,11 +86,13 @@ public class PersonalizationPage extends StackPane {
} }
{ {
StackPane componentList = new StackPane(); ComponentList componentList = new ComponentList();
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>(); MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>();
backgroundItem.setTitle(i18n("launcher.background")); ComponentSublist backgroundSublist = new ComponentSublist();
backgroundItem.setHasSubtitle(true); backgroundSublist.getContent().add(backgroundItem);
backgroundSublist.setTitle(i18n("launcher.background"));
backgroundSublist.setHasSubtitle(true);
backgroundItem.loadChildren(Arrays.asList( backgroundItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT), new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
@ -102,12 +104,12 @@ public class PersonalizationPage extends StackPane {
.bindBidirectional(config().backgroundImageUrlProperty()) .bindBidirectional(config().backgroundImageUrlProperty())
)); ));
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty()); backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind( backgroundSublist.subtitleProperty().bind(
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT)) new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default")) .then(i18n("launcher.background.default"))
.otherwise(config().backgroundImageProperty())); .otherwise(config().backgroundImageProperty()));
componentList.getChildren().add(backgroundItem); componentList.getContent().add(backgroundItem);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList); content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
} }

View File

@ -64,7 +64,7 @@ public final class SettingsPage extends SettingsView {
// ==== // ====
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty()); fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.subtitleProperty().bind( fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory()) Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")), .orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty())); config().commonDirectoryProperty(), config().commonDirTypeProperty()));

View File

@ -48,6 +48,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class SettingsView extends StackPane { public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage; protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation; protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate; protected final Label lblUpdate;
protected final Label lblUpdateSub; protected final Label lblUpdateSub;
protected final JFXRadioButton chkUpdateStable; protected final JFXRadioButton chkUpdateStable;
@ -144,8 +145,10 @@ public abstract class SettingsView extends StackPane {
{ {
fileCommonLocation = new MultiFileItem<>(); fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.setTitle(i18n("launcher.cache_directory")); fileCommonLocationSublist = new ComponentSublist();
fileCommonLocation.setHasSubtitle(true); fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocation.loadChildren(Arrays.asList( fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT), new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM) new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
@ -159,10 +162,10 @@ public abstract class SettingsView extends StackPane {
cleanButton.setOnMouseClicked(e -> clearCacheDirectory()); cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
cleanButton.getStyleClass().add("jfx-button-border"); cleanButton.getStyleClass().add("jfx-button-border");
fileCommonLocation.setHeaderRight(cleanButton); fileCommonLocationSublist.setHeaderRight(cleanButton);
} }
settingsPane.getContent().add(fileCommonLocation); settingsPane.getContent().add(fileCommonLocationSublist);
} }
{ {

View File

@ -99,10 +99,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final OptionToggleButton noJVMCheckPane; private final OptionToggleButton noJVMCheckPane;
private final OptionToggleButton useNativeGLFWPane; private final OptionToggleButton useNativeGLFWPane;
private final OptionToggleButton useNativeOpenALPane; private final OptionToggleButton useNativeOpenALPane;
private final ComponentSublist javaSublist;
private final MultiFileItem<JavaVersion> javaItem; private final MultiFileItem<JavaVersion> javaItem;
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption; private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
private final ComponentSublist gameDirSublist;
private final MultiFileItem<GameDirectoryType> gameDirItem; private final MultiFileItem<GameDirectoryType> gameDirItem;
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption; private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem; private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption; private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<ProcessPriority> cboProcessPriority; private final JFXComboBox<ProcessPriority> cboProcessPriority;
@ -192,14 +195,18 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList.setDepth(1); componentList.setDepth(1);
javaItem = new MultiFileItem<>(); javaItem = new MultiFileItem<>();
javaItem.setTitle(i18n("settings.game.java_directory")); javaSublist = new ComponentSublist();
javaItem.setHasSubtitle(true); javaSublist.getContent().add(javaItem);
javaSublist.setTitle(i18n("settings.game.java_directory"));
javaSublist.setHasSubtitle(true);
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null) javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
.setChooserTitle(i18n("settings.game.java_directory.choose")); .setChooserTitle(i18n("settings.game.java_directory.choose"));
gameDirItem = new MultiFileItem<>(); gameDirItem = new MultiFileItem<>();
gameDirItem.setTitle(i18n("settings.game.working_directory")); gameDirSublist = new ComponentSublist();
gameDirItem.setHasSubtitle(true); gameDirSublist.getContent().add(gameDirItem);
gameDirSublist.setTitle(i18n("settings.game.working_directory"));
gameDirSublist.setHasSubtitle(true);
gameDirItem.disableProperty().bind(modpack); gameDirItem.disableProperty().bind(modpack);
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM) gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.game.working_directory.choose")) .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); 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(); HBox advancedHintPane = new HBox();
@ -474,8 +481,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
workaroundPane.disableProperty().bind(enableSpecificSettings.not()); workaroundPane.disableProperty().bind(enableSpecificSettings.not());
{ {
nativesDirItem = new MultiFileItem<>(); nativesDirItem = new MultiFileItem<>();
nativesDirItem.setTitle(i18n("settings.advanced.natives_directory")); nativesDirSublist = new ComponentSublist();
nativesDirItem.setHasSubtitle(true); nativesDirSublist.getContent().add(nativesDirItem);
nativesDirSublist.setTitle(i18n("settings.advanced.natives_directory"));
nativesDirSublist.setHasSubtitle(true);
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), NativesDirectoryType.CUSTOM) nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), NativesDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.advanced.natives_directory.choose")) .setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
.setDirectory(true); .setDirectory(true);
@ -499,7 +508,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
useNativeOpenALPane = new OptionToggleButton(); useNativeOpenALPane = new OptionToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal")); 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, rootPane.getChildren().addAll(componentList,
@ -613,10 +622,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting.javaProperty().removeListener(javaListener); lastVersionSetting.javaProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty()); gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind(); gameDirSublist.subtitleProperty().unbind();
nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty()); nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().unbind(); nativesDirSublist.subtitleProperty().unbind();
} }
// unbind data fields // unbind data fields
@ -663,11 +672,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
versionSetting.javaProperty().addListener(javaListener); versionSetting.javaProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty()); 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())); versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty()); 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())); versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
lastVersionSetting = versionSetting; lastVersionSetting = versionSetting;
@ -703,7 +712,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionSetting == null) if (versionSetting == null)
return; return;
Task.supplyAsync(versionSetting::getJavaVersion) 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"))) .map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
.start(); .start();
} }

View File

@ -91,6 +91,10 @@
-fx-fill: #856404; -fx-fill: #856404;
} }
.skin-pane .jfx-text-field {
-fx-pref-width: 200;
}
.memory-label { .memory-label {
} }

View File

@ -50,6 +50,7 @@ about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/) about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=Accounts account=Accounts
account.cape=Cape
account.character=character account.character=character
account.choose=Choose a character account.choose=Choose a character
account.create=Create a new account account.create=Create a new account
@ -110,7 +111,13 @@ account.missing=No Account
account.missing.add=Click here to add account.missing.add=Click here to add
account.not_logged_in=Not logged in account.not_logged_in=Not logged in
account.password=Password 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=Upload skin
account.skin.upload.failed=Failed to upload skin account.skin.upload.failed=Failed to upload skin
account.skin.invalid_skin=Unrecognized skin file account.skin.invalid_skin=Unrecognized skin file
@ -459,6 +466,7 @@ main_page=Home
message.cancelled=Operation was cancelled message.cancelled=Operation was cancelled
message.confirm=Confirm message.confirm=Confirm
message.copied=Copied to clipboard message.copied=Copied to clipboard
message.default=Default
message.doing=Please wait message.doing=Please wait
message.downloading=Downloading... message.downloading=Downloading...
message.error=Error message.error=Error

View File

@ -35,14 +35,14 @@ about.thanks_to=鳴謝
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載源,請贊助支持 BMCLAPI about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載源,請贊助支持 BMCLAPI
about.thanks_to.contributors=所有通過 Issues、Pull Requests 等管道參與本項目的貢獻者 about.thanks_to.contributors=所有通過 Issues、Pull Requests 等管道參與本項目的貢獻者
about.thanks_to.contributors.statement=沒有開源社區的支持Hello Minecraft! Launcher 無法走到今天 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=MCBBS 我的世界中文論壇
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 about.thanks_to.mcbbs.statement=提供 MCBBS 下載源
about.thanks_to.mcmod=MC 百科 about.thanks_to.mcmod=MC 百科
about.thanks_to.mcmod.statement=提供 Mod 中文名映射表與 Mod 百科 about.thanks_to.mcmod.statement=提供 Mod 中文名映射表與 Mod 百科
about.thanks_to.noin=這裡 (noin.cn) about.thanks_to.noin=這裡 (noin.cn)
about.thanks_to.noin.statement=提供多人聯機服務 (cato - ioi 系列作品) 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=HMCL 用戶群成員
about.thanks_to.users.statement=感謝用戶群成員贊助充電、積極催更、迴響問題、出謀劃策 about.thanks_to.users.statement=感謝用戶群成員贊助充電、積極催更、迴響問題、出謀劃策
about.thanks_to.yushijinhun.statement=authlib-injector 相关支援 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/) about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=帳戶 account=帳戶
account.cape=披風
account.character=角色 account.character=角色
account.choose=請選擇角色 account.choose=請選擇角色
account.create=建立帳戶 account.create=建立帳戶
@ -110,7 +111,13 @@ account.missing=沒有遊戲帳戶
account.missing.add=按一下此處加入帳戶 account.missing.add=按一下此處加入帳戶
account.not_logged_in=未登入 account.not_logged_in=未登入
account.password=密碼 account.password=密碼
account.skin=皮膚
account.skin.file=皮膚圖片檔案 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=上傳皮膚
account.skin.upload.failed=皮膚上傳失敗 account.skin.upload.failed=皮膚上傳失敗
account.skin.invalid_skin=無法識別的皮膚檔案 account.skin.invalid_skin=無法識別的皮膚檔案
@ -459,6 +466,7 @@ main_page=首頁
message.cancelled=操作被取消 message.cancelled=操作被取消
message.confirm=提示 message.confirm=提示
message.copied=已複製到剪貼板 message.copied=已複製到剪貼板
message.default=預設
message.doing=請耐心等待 message.doing=請耐心等待
message.downloading=正在下載… message.downloading=正在下載…
message.error=錯誤 message.error=錯誤
@ -708,14 +716,14 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
settings.advanced.java_permanent_generation_space.prompt=格式: MB settings.advanced.java_permanent_generation_space.prompt=格式: MB
settings.advanced.jvm=Java 虛擬機設定 settings.advanced.jvm=Java 虛擬機設定
settings.advanced.jvm_args=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.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器 settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟 settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器 settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性 settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數 settings.advanced.minecraft_arguments=Minecraft 額外參數
settings.advanced.minecraft_arguments.prompt=默認 settings.advanced.minecraft_arguments.prompt=預設
settings.advanced.natives_directory=本地庫路徑LWJGL settings.advanced.natives_directory=本地庫路徑LWJGL
settings.advanced.natives_directory.choose=選擇本地庫路徑 settings.advanced.natives_directory.choose=選擇本地庫路徑
settings.advanced.natives_directory.default=預設(.minecraft/versions/<版本名>/natives/ 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=遊戲結束後執行命令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用 settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.server_ip=伺服器位址 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_glfw=[Linux] 使用系統 GLFW
settings.advanced.use_native_openal=[Linux] 使用系統 OpenAL settings.advanced.use_native_openal=[Linux] 使用系統 OpenAL
settings.advanced.workaround=除錯選項 settings.advanced.workaround=除錯選項

View File

@ -50,6 +50,7 @@ about.open_source=开源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/) about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=帐户 account=帐户
account.cape=披风
account.character=角色 account.character=角色
account.choose=选择一个角色 account.choose=选择一个角色
account.create=添加帐户 account.create=添加帐户
@ -110,7 +111,13 @@ account.missing=没有游戏帐户
account.missing.add=点击此处添加帐户 account.missing.add=点击此处添加帐户
account.not_logged_in=未登录 account.not_logged_in=未登录
account.password=密码 account.password=密码
account.skin=皮肤
account.skin.file=皮肤图片文件 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=上传皮肤
account.skin.upload.failed=皮肤上传失败 account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件 account.skin.invalid_skin=无法识别的皮肤文件
@ -459,6 +466,7 @@ main_page=主页
message.cancelled=操作被取消 message.cancelled=操作被取消
message.confirm=提示 message.confirm=提示
message.copied=已复制到剪贴板 message.copied=已复制到剪贴板
message.default=默认
message.doing=请耐心等待 message.doing=请耐心等待
message.downloading=正在下载 message.downloading=正在下载
message.error=错误 message.error=错误

View File

@ -1,7 +1,5 @@
package moe.mickey.minecraft.skin.fx.test; package moe.mickey.minecraft.skin.fx.test;
import java.util.function.Consumer;
import javafx.application.Application; import javafx.application.Application;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; 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.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms; import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import java.util.function.Consumer;
public class Test extends Application { public class Test extends Application {
public static final String TITLE = "FX - Minecraft skin preview"; public static final String TITLE = "FX - Minecraft skin preview";
public static SkinCanvas createSkinCanvas() { 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)); 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)); FunctionHelper.alwaysB(Consumer<SkinCanvas>::accept, canvas, new SkinCanvasSupport.Mouse(.5), new SkinCanvasSupport.Drag(TITLE));
return canvas; return canvas;

View File

@ -17,13 +17,14 @@
*/ */
package org.jackhuang.hmcl.auth.offline; package org.jackhuang.hmcl.auth.offline;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException; 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.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
@ -51,13 +52,13 @@ public class OfflineAccount extends Account {
private final AuthlibInjectorArtifactProvider downloader; private final AuthlibInjectorArtifactProvider downloader;
private final String username; private final String username;
private final UUID uuid; 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.downloader = requireNonNull(downloader);
this.username = requireNonNull(username); this.username = requireNonNull(username);
this.uuid = requireNonNull(uuid); this.uuid = requireNonNull(uuid);
this.textures = textures; this.skin = skin;
if (StringUtils.isBlank(username)) { if (StringUtils.isBlank(username)) {
throw new IllegalArgumentException("Username cannot be blank"); throw new IllegalArgumentException("Username cannot be blank");
@ -79,11 +80,20 @@ public class OfflineAccount extends Account {
return username; return username;
} }
public Skin getSkin() {
return skin;
}
public void setSkin(Skin skin) {
this.skin = skin;
invalidate();
}
@Override @Override
public AuthInfo logIn() throws AuthenticationException { public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}"); AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
if (skin != null || cape != null) { if (skin != null) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> { CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try { try {
return downloader.getArtifactInfo(); return downloader.getArtifactInfo();
@ -109,18 +119,14 @@ public class OfflineAccount extends Account {
try { try {
YggdrasilServer server = new YggdrasilServer(0); YggdrasilServer server = new YggdrasilServer(0);
server.start(); server.start();
server.addCharacter(new YggdrasilServer.Character(uuid, username, TextureModel.STEVE, server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
mapOf(
pair(TextureType.SKIN, server.loadTexture(skin)),
pair(TextureType.CAPE, server.loadTexture(cape))
)));
return authInfo.withArguments(new Arguments().addJVMArguments( return authInfo.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(), "-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
"-Dauthlibinjector.side=client" "-Dauthlibinjector.side=client"
)) ))
.withCloseable(server::stop); .withCloseable(server::stop);
} catch (IOException e) { } catch (Exception e) {
throw new AuthenticationException(e); throw new AuthenticationException(e);
} }
} else { } else {
@ -143,18 +149,20 @@ public class OfflineAccount extends Account {
return mapOf( return mapOf(
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)), pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
pair("username", username), pair("username", username),
pair("skin", skin), pair("skin", skin.toStorage())
pair("cape", cape)
); );
} }
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return super.getTextures();
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)
.append("username", username) .append("username", username)
.append("uuid", uuid) .append("uuid", uuid)
.append("skin", skin)
.append("cape", cape)
.toString(); .toString();
} }

View File

@ -45,25 +45,23 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
} }
public OfflineAccount create(String username, UUID uuid) { public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(downloader, username, uuid, null, null); return new OfflineAccount(downloader, username, uuid, null);
} }
@Override @Override
public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) { public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) {
AdditionalData data; AdditionalData data;
UUID uuid; UUID uuid;
String skin; Skin skin;
String cape;
if (additionalData != null) { if (additionalData != null) {
data = (AdditionalData) additionalData; data = (AdditionalData) additionalData;
uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid; uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid;
skin = data.skin; skin = data.skin;
cape = data.cape;
} else { } else {
uuid = getUUIDFromUserName(username); uuid = getUUIDFromUserName(username);
skin = cape = null; skin = null;
} }
return new OfflineAccount(downloader, username, uuid, skin, cape); return new OfflineAccount(downloader, username, uuid, skin);
} }
@Override @Override
@ -73,10 +71,9 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
UUID uuid = tryCast(storage.get("uuid"), String.class) UUID uuid = tryCast(storage.get("uuid"), String.class)
.map(UUIDTypeAdapter::fromString) .map(UUIDTypeAdapter::fromString)
.orElse(getUUIDFromUserName(username)); .orElse(getUUIDFromUserName(username));
String skin = tryCast(storage.get("skin"), String.class).orElse(null); Skin skin = Skin.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null));
String cape = tryCast(storage.get("cape"), String.class).orElse(null);
return new OfflineAccount(downloader, username, uuid, skin, cape); return new OfflineAccount(downloader, username, uuid, skin);
} }
public static UUID getUUIDFromUserName(String username) { public static UUID getUUIDFromUserName(String username) {
@ -85,13 +82,11 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
public static class AdditionalData { public static class AdditionalData {
private final UUID uuid; private final UUID uuid;
private final String skin; private final Skin skin;
private final String cape;
public AdditionalData(UUID uuid, String skin, String cape) { public AdditionalData(UUID uuid, Skin skin) {
this.uuid = uuid; this.uuid = uuid;
this.skin = skin; this.skin = skin;
this.cape = cape;
} }
} }

View File

@ -18,26 +18,31 @@
package org.jackhuang.hmcl.auth.offline; package org.jackhuang.hmcl.auth.offline;
import com.google.gson.annotations.SerializedName; 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.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.task.FetchTask; import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable; 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.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import java.util.Locale;
import java.util.Map; 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.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Skin { public class Skin {
@ -46,33 +51,81 @@ public class Skin {
STEVE, STEVE,
ALEX, ALEX,
LOCAL_FILE, LOCAL_FILE,
LITTLE_SKIN,
CUSTOM_SKIN_LOADER_API, 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 final Type type;
private String value; 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() { public Type getType() {
return type; return type;
} }
public String getValue() { public String getCslApi() {
return value; 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) { switch (type) {
case DEFAULT: case DEFAULT:
return Task.supplyAsync(() -> null); return Task.supplyAsync(() -> null);
case STEVE: 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: 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: 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: 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 -> { .thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class); SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@ -80,13 +133,57 @@ public class Skin {
return Task.supplyAsync(() -> null); return Task.supplyAsync(() -> null);
} }
return new FetchBytesTask(new URL(String.format("%s/textures/%s", value, result.getHash())), 3); return Task.allOf(
}).thenApplyAsync(Texture::loadTexture); 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: default:
throw new UnsupportedOperationException(); 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> { private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) { 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 static class SkinJson {
private final String username; private final String username;
private final String skin; private final String skin;
@ -183,6 +304,12 @@ public class Skin {
return null; return null;
} }
public String getCapeHash() {
if (textures != null && textures.cape != null) {
return textures.cape;
} else return cape;
}
public static class TextureJson { public static class TextureJson {
@SerializedName("default") @SerializedName("default")
private final String defaultSkin; private final String defaultSkin;

View File

@ -42,6 +42,10 @@ public class Texture {
this.data = requireNonNull(data); this.data = requireNonNull(data);
} }
public byte[] getData() {
return data;
}
public String getHash() { public String getHash() {
return hash; return hash;
} }

View File

@ -19,8 +19,6 @@ package org.jackhuang.hmcl.auth.offline;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; 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.KeyUtils;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
@ -36,7 +34,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8; 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.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
@ -143,14 +140,12 @@ public class YggdrasilServer extends HttpServer {
public static class Character { public static class Character {
private final UUID uuid; private final UUID uuid;
private final String name; private final String name;
private final TextureModel model; private final Skin.LoadedSkin skin;
private final Map<TextureType, Texture> textures;
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.uuid = uuid;
this.name = name; this.name = name;
this.model = model; this.skin = Objects.requireNonNull(skin);
this.textures = textures;
} }
public UUID getUUID() { public UUID getUUID() {
@ -161,30 +156,17 @@ public class YggdrasilServer extends HttpServer {
return name; 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() { public GameProfile toSimpleResponse() {
return new GameProfile(uuid, name); return new GameProfile(uuid, name);
} }
public Object toCompleteResponse(String rootUrl) { public Object toCompleteResponse(String rootUrl) {
Map<String, Object> realTextures = new HashMap<>(); Map<String, Object> realTextures = new HashMap<>();
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) { if (skin.getSkin() != null) {
if (textureEntry.getValue() == null) continue; realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().getHash()))); }
if (skin.getCape() != null) {
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
} }
Map<String, Object> textureResponse = mapOf( Map<String, Object> textureResponse = mapOf(

View File

@ -340,7 +340,7 @@ public abstract class Task<T> {
messageUpdate.accept(newMessage); messageUpdate.accept(newMessage);
} }
public final void run() throws Exception { public final T run() throws Exception {
if (getSignificance().shouldLog()) if (getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + getName()); Logging.LOG.log(Level.FINE, "Executing task: " + getName());
@ -350,6 +350,8 @@ public abstract class Task<T> {
for (Task<?> task : getDependencies()) for (Task<?> task : getDependencies())
doSubTask(task); doSubTask(task);
onDone.fireEvent(new TaskEvent(this, this, false)); onDone.fireEvent(new TaskEvent(this, this, false));
return getResult();
} }
private void doSubTask(Task<?> task) throws Exception { private void doSubTask(Task<?> task) throws Exception {

View File

@ -413,6 +413,7 @@ public final class FileUtils {
} }
public static Optional<Path> tryGetPath(String first, String... more) { public static Optional<Path> tryGetPath(String first, String... more) {
if (first == null) return Optional.empty();
try { try {
return Optional.of(Paths.get(first, more)); return Optional.of(Paths.get(first, more));
} catch (InvalidPathException e) { } catch (InvalidPathException e) {