From 0867961e3649a5dedb3370e4102e7ebb8cd5a3a4 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 09:52:20 +0800 Subject: [PATCH 01/24] =?UTF-8?q?=E7=BB=8F=E8=B5=B7=E5=B9=BD=E6=98=8E=20?= =?UTF-8?q?=E6=82=9F=E5=A4=84=E9=80=9A=E7=8E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 13 +- .../ui/account/OfflineAccountSkinPane.java | 57 +--- .../org/jackhuang/hmcl/auth/offline/Skin.java | 260 ++---------------- .../hmcl/auth/offline/YggdrasilServer.java | 12 +- 4 files changed, 47 insertions(+), 295 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index ab0c15dd72a..9bd60266492 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -45,7 +45,10 @@ import java.lang.ref.WeakReference; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -209,15 +212,15 @@ public static ObservableValue skinBinding(Account account) { skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to load texture", exception); - } else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) { + } else if (result != null && result.skin() != null && result.skin().getImage() != null) { Map metadata; - if (result.getModel() != null) { - metadata = singletonMap("model", result.getModel().modelName); + if (result.model() != null) { + metadata = singletonMap("model", result.model().modelName); } else { metadata = emptyMap(); } - binding.set(new LoadedTexture(result.getSkin().getImage(), metadata)); + binding.set(new LoadedTexture(result.skin().getImage(), metadata)); } }).start(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index 25467e7e151..1cc6cd331b0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -20,11 +20,9 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXTextField; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.geometry.Insets; -import javafx.geometry.VPos; import javafx.scene.control.Label; import javafx.scene.input.DragEvent; import javafx.scene.input.TransferMode; @@ -36,7 +34,10 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.FileSelector; +import org.jackhuang.hmcl.ui.construct.JFXHyperlink; +import org.jackhuang.hmcl.ui.construct.MultiFileItem; import org.jackhuang.hmcl.ui.skin.SkinCanvas; import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning; import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms; @@ -55,7 +56,6 @@ public class OfflineAccountSkinPane extends StackPane { private final OfflineAccount account; private final MultiFileItem skinItem = new MultiFileItem<>(); - private final JFXTextField cslApiField = new JFXTextField(); private final JFXComboBox modelCombobox = new JFXComboBox<>(); private final FileSelector skinSelector = new FileSelector(); private final FileSelector capeSelector = new FileSelector(); @@ -108,17 +108,11 @@ public OfflineAccountSkinPane(OfflineAccount account) { layout.setBody(pane); - cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint")); - cslApiField.setValidators(new URLValidator()); - FXUtils.setValidateWhileTextChanged(cslApiField, true); - skinItem.loadChildren(Arrays.asList( new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT), new MultiFileItem.Option<>(i18n("account.skin.type.steve"), Skin.Type.STEVE), new MultiFileItem.Option<>(i18n("account.skin.type.alex"), Skin.Type.ALEX), - new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE), - new MultiFileItem.Option<>(i18n("account.skin.type.little_skin"), Skin.Type.LITTLE_SKIN), - new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API) + new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE) )); modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName))); @@ -129,7 +123,6 @@ public OfflineAccountSkinPane(OfflineAccount account) { modelCombobox.setValue(TextureModel.WIDE); } else { skinItem.setSelectedData(account.getSkin().getType()); - cslApiField.setText(account.getSkin().getCslApi()); modelCombobox.setValue(account.getSkin().getTextureModel()); skinSelector.setValue(account.getSkin().getLocalSkinPath()); capeSelector.setValue(account.getSkin().getLocalCapePath()); @@ -143,7 +136,7 @@ public OfflineAccountSkinPane(OfflineAccount account) { Controllers.showToast(i18n("message.failed")); } else { UUID uuid = this.account.getUUID(); - if (result == null || result.getSkin() == null && result.getCape() == null) { + if (result == null || result.skin() == null && result.cape() == null) { canvas.updateSkin( TexturesLoader.getDefaultSkin(uuid).getImage(), TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM, @@ -152,12 +145,12 @@ public OfflineAccountSkinPane(OfflineAccount account) { return; } canvas.updateSkin( - result.getSkin() != null ? result.getSkin().getImage() : TexturesLoader.getDefaultSkin(uuid).getImage(), - result.getModel() == TextureModel.SLIM, - result.getCape() != null ? result.getCape().getImage() : null); + result.skin() != null ? result.skin().getImage() : TexturesLoader.getDefaultSkin(uuid).getImage(), + result.model() == TextureModel.SLIM, + result.cape() != null ? result.cape().getImage() : null); } }).start(); - }, skinItem.selectedDataProperty(), cslApiField.textProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); + }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> { GridPane gridPane = new GridPane(); @@ -173,33 +166,12 @@ public OfflineAccountSkinPane(OfflineAccount account) { case STEVE: case ALEX: break; - case LITTLE_SKIN: - HintPane hint = new HintPane(MessageDialogPane.MessageType.INFO); - hint.setText(i18n("account.skin.type.little_skin.hint")); - - // Spanning two columns and expanding horizontally - GridPane.setColumnSpan(hint, 2); - GridPane.setHgrow(hint, Priority.ALWAYS); - hint.setMaxWidth(Double.MAX_VALUE); - - // Force top alignment within cells (to avoid vertical offset caused by the baseline) - GridPane.setValignment(hint, VPos.TOP); - - // Set a fixed height as the preferred height to prevent the GridPane from stretching or leaving empty space. - hint.setMaxHeight(Region.USE_PREF_SIZE); - hint.setMinHeight(Region.USE_PREF_SIZE); - - gridPane.addRow(0, hint); - break; case LOCAL_FILE: gridPane.setPadding(new Insets(0, 0, 0, 10)); gridPane.addRow(0, new Label(i18n("account.skin.model")), modelCombobox); gridPane.addRow(1, new Label(i18n("account.skin")), skinSelector); gridPane.addRow(2, 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.getChildren().setAll(gridPane); @@ -219,20 +191,15 @@ public OfflineAccountSkinPane(OfflineAccount account) { cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); onEscPressed(this, cancelButton::fire); - acceptButton.disableProperty().bind( - skinItem.selectedDataProperty().isEqualTo(Skin.Type.CUSTOM_SKIN_LOADER_API) - .and(cslApiField.activeValidatorProperty().isNotNull())); - layout.setActions(littleSkinLink, acceptButton, cancelButton); } private Skin getSkin() { Skin.Type type = skinItem.getSelectedData(); if (type == Skin.Type.LOCAL_FILE) { - return new Skin(type, cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); + return new Skin(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); } else { - String cslApi = type == Skin.Type.CUSTOM_SKIN_LOADER_API ? cslApiField.getText() : null; - return new Skin(type, cslApi, null, null, null); + return new Skin(type, null, null, null); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java index eac62d2b402..e1c4c75ab91 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java @@ -17,27 +17,16 @@ */ package org.jackhuang.hmcl.auth.offline; -import com.google.gson.annotations.SerializedName; import javafx.scene.image.Image; import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; -import org.jackhuang.hmcl.task.FetchTask; -import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; -import org.jetbrains.annotations.Nullable; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +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; @@ -56,56 +45,33 @@ public enum Type { STEVE, SUNNY, ZURI, - LOCAL_FILE, - LITTLE_SKIN, - CUSTOM_SKIN_LOADER_API, - YGGDRASIL_API; + LOCAL_FILE; public static Type fromStorage(String type) { - switch (type) { - case "default": - return DEFAULT; - case "alex": - return ALEX; - case "ari": - return ARI; - case "efe": - return EFE; - case "kai": - return KAI; - case "makena": - return MAKENA; - case "noor": - return NOOR; - case "steve": - return STEVE; - case "sunny": - return SUNNY; - case "zuri": - return ZURI; - 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; - } + return switch (type) { + case "default" -> DEFAULT; + case "alex" -> ALEX; + case "ari" -> ARI; + case "efe" -> EFE; + case "kai" -> KAI; + case "makena" -> MAKENA; + case "noor" -> NOOR; + case "steve" -> STEVE; + case "sunny" -> SUNNY; + case "zuri" -> ZURI; + case "local_file" -> LOCAL_FILE; + default -> null; + }; } } private final Type type; - private final String cslApi; private final TextureModel textureModel; private final String localSkinPath; private final String localCapePath; - public Skin(Type type, String cslApi, TextureModel textureModel, String localSkinPath, String localCapePath) { + public Skin(Type type, TextureModel textureModel, String localSkinPath, String localCapePath) { this.type = type; - this.cslApi = cslApi; this.textureModel = textureModel; this.localSkinPath = localSkinPath; this.localCapePath = localCapePath; @@ -115,10 +81,6 @@ public Type getType() { return type; } - public String getCslApi() { - return cslApi; - } - public TextureModel getTextureModel() { return textureModel == null ? TextureModel.WIDE : textureModel; } @@ -161,44 +123,6 @@ public Task load(String username) { if (capePath.isPresent()) cape = Texture.loadTexture(Files.newInputStream(capePath.get())); return new LoadedSkin(getTextureModel(), skin, cape); }); - case LITTLE_SKIN: - case CUSTOM_SKIN_LOADER_API: - String realCslApi = type == Type.LITTLE_SKIN - ? "https://littleskin.cn/csl" - : NetworkUtils.addHttpsIfMissing(StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/")); - return Task.composeAsync(() -> new GetTask(String.format("%s/%s.json", realCslApi, username))) - .thenComposeAsync(json -> { - SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class); - - if (!result.hasSkin()) { - return Task.supplyAsync(() -> null); - } - - return Task.allOf( - Task.supplyAsync(result::getModel), - result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(String.format("%s/textures/%s", realCslApi, result.getHash())), - result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(String.format("%s/textures/%s", realCslApi, result.getCapeHash())) - ); - }).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(); } @@ -207,7 +131,6 @@ public Task load(String username) { public Map toStorage() { return mapOf( pair("type", type.name().toLowerCase(Locale.ROOT)), - pair("cslApi", cslApi), pair("textureModel", getTextureModel().modelName), pair("localSkinPath", localSkinPath), pair("localCapePath", localCapePath) @@ -219,154 +142,13 @@ public static Skin fromStorage(Map storage) { 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 textureModel = tryCast(storage.get("textureModel"), String.class).orElse("default"); 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, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath); - } - - private static class FetchBytesTask extends FetchTask { - - public FetchBytesTask(String uri) { - super(List.of(NetworkUtils.toURI(uri))); - } - - @Override - protected void useCachedResult(Path cachedFile) throws IOException { - setResult(Files.newInputStream(cachedFile)); - } - - @Override - protected EnumCheckETag shouldCheckETag() { - return EnumCheckETag.CHECK_E_TAG; - } - - @Override - protected Context getContext(HttpResponse response, boolean checkETag, String bmclapiHash) throws IOException { - return new Context() { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - @Override - public void write(byte[] buffer, int offset, int len) { - baos.write(buffer, offset, len); - } - - @Override - public void close() throws IOException { - if (!isSuccess()) return; - - setResult(new ByteArrayInputStream(baos.toByteArray())); - - if (checkETag) { - repository.cacheBytes(response, baos.toByteArray()); - } - } - }; - } - } - - 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; - } + return new Skin(type, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath); } - private static class SkinJson { - private final String username; - private final String skin; - private final String cape; - private final String elytra; - - @SerializedName(value = "textures", alternate = { "skins" }) - private final TextureJson textures; - - public SkinJson(String username, String skin, String cape, String elytra, TextureJson textures) { - this.username = username; - this.skin = skin; - this.cape = cape; - this.elytra = elytra; - this.textures = textures; - } - - public boolean hasSkin() { - return StringUtils.isNotBlank(username); - } - - @Nullable - public TextureModel getModel() { - if (textures != null && textures.slim != null) { - return TextureModel.SLIM; - } else if (textures != null && textures.defaultSkin != null) { - return TextureModel.WIDE; - } else { - return null; - } - } - - public String getAlexModelHash() { - if (textures != null && textures.slim != null) { - return textures.slim; - } else { - return null; - } - } - - public String getSteveModelHash() { - if (textures != null && textures.defaultSkin != null) { - return textures.defaultSkin; - } else return skin; - } - - public String getHash() { - TextureModel model = getModel(); - if (model == TextureModel.SLIM) - return getAlexModelHash(); - else if (model == TextureModel.WIDE) - return getSteveModelHash(); - else - 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; - - private final String slim; - private final String cape; - private final String elytra; - - public TextureJson(String defaultSkin, String slim, String cape, String elytra) { - this.defaultSkin = defaultSkin; - this.slim = slim; - this.cape = cape; - this.elytra = elytra; - } - } + public record LoadedSkin(TextureModel model, Texture skin, Texture cape) { } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 70a9976d32e..268d36a6dce 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -177,19 +177,19 @@ public GameProfile toSimpleResponse() { public Object toCompleteResponse(String rootUrl) { Map realTextures = new HashMap<>(); - if (skin != null && skin.getSkin() != null) { - if (skin.getModel() == TextureModel.SLIM) { + if (skin != null && skin.skin() != null) { + if (skin.model() == TextureModel.SLIM) { realTextures.put("SKIN", mapOf( - pair("url", rootUrl + "/textures/" + skin.getSkin().getHash()), + pair("url", rootUrl + "/textures/" + skin.skin().getHash()), pair("metadata", mapOf( pair("model", "slim") )))); } else { - realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash()))); + realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.skin().getHash()))); } } - if (skin != null && skin.getCape() != null) { - realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getCape().getHash()))); + if (skin != null && skin.cape() != null) { + realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.cape().getHash()))); } Map textureResponse = mapOf( From 5b6fc2f4255caf8c008cdb92af79dff9456f1b3a Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 09:54:22 +0800 Subject: [PATCH 02/24] =?UTF-8?q?=E9=A6=96=E7=AA=A5=E9=BE=99=E5=A0=91=20?= =?UTF-8?q?=E8=A7=81=E5=B2=B3=E8=A7=81=E6=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 31 +++++----------- .../ui/account/OfflineAccountSkinPane.java | 14 ++++---- .../hmcl/auth/offline/OfflineAccount.java | 7 ++-- .../org/jackhuang/hmcl/auth/offline/Skin.java | 35 ++++--------------- .../hmcl/auth/offline/YggdrasilServer.java | 5 +-- 5 files changed, 27 insertions(+), 65 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index 9bd60266492..dc2e0687009 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -68,22 +68,11 @@ private TexturesLoader() { } // ==== Texture Loading ==== - public static class LoadedTexture { - private final Image image; - private final Map metadata; - + public record LoadedTexture(Image image, Map metadata) { public LoadedTexture(Image image, Map metadata) { this.image = requireNonNull(image); this.metadata = requireNonNull(metadata); } - - public Image getImage() { - return image; - } - - public Map getMetadata() { - return metadata; - } } private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS); @@ -161,7 +150,7 @@ public static LoadedTexture getDefaultSkin(UUID uuid) { } public static TextureModel getDefaultModel(UUID uuid) { - return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model")) + return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).metadata().get("model")) ? TextureModel.WIDE : TextureModel.SLIM; } @@ -199,8 +188,7 @@ public static ObjectBinding skinBinding(YggdrasilService service, public static ObservableValue skinBinding(Account account) { LoadedTexture uuidFallback = getDefaultSkin(account.getUUID()); - if (account instanceof OfflineAccount) { - OfflineAccount offlineAccount = (OfflineAccount) account; + if (account instanceof OfflineAccount offlineAccount) { SimpleObjectProperty binding = new SimpleObjectProperty<>(); InvalidationListener listener = o -> { @@ -209,7 +197,7 @@ public static ObservableValue skinBinding(Account account) { binding.set(uuidFallback); if (skin != null) { - skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> { + skin.load().setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to load texture", exception); } else if (result != null && result.skin() != null && result.skin().getImage() != null) { @@ -278,15 +266,12 @@ private static void drawAvatar(GraphicsContext g, Image skin, int size, int scal 0, 0, size, size); } - private static final class SkinBindingChangeListener implements ChangeListener { + private record SkinBindingChangeListener(WeakReference canvasRef, + ObservableValue binding) implements ChangeListener { static final WeakHashMap hole = new WeakHashMap<>(); - final WeakReference canvasRef; - final ObservableValue binding; - - SkinBindingChangeListener(Canvas canvas, ObservableValue binding) { - this.canvasRef = new WeakReference<>(canvas); - this.binding = binding; + private SkinBindingChangeListener(Canvas canvasRef, ObservableValue binding) { + this(new WeakReference<>(canvasRef), binding); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index 1cc6cd331b0..c1d13efb804 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -122,14 +122,14 @@ public OfflineAccountSkinPane(OfflineAccount account) { skinItem.setSelectedData(Skin.Type.DEFAULT); modelCombobox.setValue(TextureModel.WIDE); } else { - skinItem.setSelectedData(account.getSkin().getType()); - modelCombobox.setValue(account.getSkin().getTextureModel()); - skinSelector.setValue(account.getSkin().getLocalSkinPath()); - capeSelector.setValue(account.getSkin().getLocalCapePath()); + skinItem.setSelectedData(account.getSkin().type()); + modelCombobox.setValue(account.getSkin().textureModel()); + skinSelector.setValue(account.getSkin().localSkinPath()); + capeSelector.setValue(account.getSkin().localCapePath()); } skinBinding = FXUtils.observeWeak(() -> { - getSkin().load(account.getUsername()) + getSkin().load() .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to load skin", exception); @@ -138,14 +138,14 @@ public OfflineAccountSkinPane(OfflineAccount account) { UUID uuid = this.account.getUUID(); if (result == null || result.skin() == null && result.cape() == null) { canvas.updateSkin( - TexturesLoader.getDefaultSkin(uuid).getImage(), + TexturesLoader.getDefaultSkin(uuid).image(), TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM, null ); return; } canvas.updateSkin( - result.skin() != null ? result.skin().getImage() : TexturesLoader.getDefaultSkin(uuid).getImage(), + result.skin() != null ? result.skin().getImage() : TexturesLoader.getDefaultSkin(uuid).image(), result.model() == TextureModel.SLIM, result.cape() != null ? result.cape().getImage() : null); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index 7e93fa9019c..70bcd486c3e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -100,7 +100,7 @@ public void setSkin(Skin skin) { } protected boolean loadAuthlibInjector(Skin skin) { - return skin != null && skin.getType() != Skin.Type.DEFAULT; + return skin != null && skin.type() != Skin.Type.DEFAULT; } public AuthInfo logInWithoutSkin() throws AuthenticationException { @@ -164,7 +164,7 @@ public Arguments getLaunchArguments(LaunchOptions options) throws IOException { try { server.addCharacter(new YggdrasilServer.Character(uuid, username, - skin != null ? skin.load(username).run() : null)); + skin != null ? skin.load().run() : null)); } catch (IOException e) { // ignore } catch (Exception e) { @@ -220,9 +220,8 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (!(obj instanceof OfflineAccount)) + if (!(obj instanceof OfflineAccount another)) return false; - OfflineAccount another = (OfflineAccount) obj; return isPortable() == another.isPortable() && username.equals(another.username); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java index e1c4c75ab91..f34532a5de0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java @@ -32,7 +32,7 @@ import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Pair.pair; -public class Skin { +public record Skin(Type type, TextureModel textureModel, String localSkinPath, String localCapePath) { public enum Type { DEFAULT, @@ -65,35 +65,12 @@ public static Type fromStorage(String type) { } } - private final Type type; - private final TextureModel textureModel; - private final String localSkinPath; - private final String localCapePath; - - public Skin(Type type, TextureModel textureModel, String localSkinPath, String localCapePath) { - this.type = type; - this.textureModel = textureModel; - this.localSkinPath = localSkinPath; - this.localCapePath = localCapePath; - } - - public Type getType() { - return type; - } - - public TextureModel getTextureModel() { + @Override + public TextureModel textureModel() { return textureModel == null ? TextureModel.WIDE : textureModel; } - public String getLocalSkinPath() { - return localSkinPath; - } - - public String getLocalCapePath() { - return localCapePath; - } - - public Task load(String username) { + public Task load() { switch (type) { case DEFAULT: return Task.supplyAsync(() -> null); @@ -121,7 +98,7 @@ public Task load(String username) { Optional 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(getTextureModel(), skin, cape); + return new LoadedSkin(textureModel(), skin, cape); }); default: throw new UnsupportedOperationException(); @@ -131,7 +108,7 @@ public Task load(String username) { public Map toStorage() { return mapOf( pair("type", type.name().toLowerCase(Locale.ROOT)), - pair("textureModel", getTextureModel().modelName), + pair("textureModel", textureModel().modelName), pair("localSkinPath", localSkinPath), pair("localCapePath", localCapePath) ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 268d36a6dce..0e20017fadf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -178,14 +178,15 @@ public GameProfile toSimpleResponse() { public Object toCompleteResponse(String rootUrl) { Map realTextures = new HashMap<>(); if (skin != null && skin.skin() != null) { + String url = rootUrl + "/textures/" + skin.skin().getHash(); if (skin.model() == TextureModel.SLIM) { realTextures.put("SKIN", mapOf( - pair("url", rootUrl + "/textures/" + skin.skin().getHash()), + pair("url", url), pair("metadata", mapOf( pair("model", "slim") )))); } else { - realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.skin().getHash()))); + realTextures.put("SKIN", mapOf(pair("url", url))); } } if (skin != null && skin.cape() != null) { From 9269ccc68242626a65960669ef27f47af7a60d95 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 09:57:21 +0800 Subject: [PATCH 03/24] =?UTF-8?q?=E9=81=93=E4=B8=8D=E5=96=84=E5=AE=A3=20?= =?UTF-8?q?=E4=B9=89=E4=B8=8D=E5=96=84=E7=BB=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 5 ++-- .../ui/account/OfflineAccountSkinPane.java | 26 +++++++++---------- .../hmcl/auth/offline/OfflineAccount.java | 12 ++++----- .../auth/offline/OfflineAccountFactory.java | 8 +++--- .../{Skin.java => OfflineSkinConfig.java} | 6 ++--- .../hmcl/auth/offline/YggdrasilServer.java | 4 +-- 6 files changed, 30 insertions(+), 31 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/{Skin.java => OfflineSkinConfig.java} (93%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index dc2e0687009..7f77bad267d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -31,7 +31,7 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.offline.Skin; +import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; import org.jackhuang.hmcl.auth.yggdrasil.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; @@ -192,8 +192,7 @@ public static ObservableValue skinBinding(Account account) { SimpleObjectProperty binding = new SimpleObjectProperty<>(); InvalidationListener listener = o -> { - Skin skin = offlineAccount.getSkin(); - String username = offlineAccount.getUsername(); + OfflineSkinConfig skin = offlineAccount.getSkin(); binding.set(uuidFallback); if (skin != null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index c1d13efb804..b45ead91c4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -28,7 +28,7 @@ import javafx.scene.input.TransferMode; import javafx.scene.layout.*; import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.offline.Skin; +import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.task.Schedulers; @@ -55,7 +55,7 @@ public class OfflineAccountSkinPane extends StackPane { private final OfflineAccount account; - private final MultiFileItem skinItem = new MultiFileItem<>(); + private final MultiFileItem skinItem = new MultiFileItem<>(); private final JFXComboBox modelCombobox = new JFXComboBox<>(); private final FileSelector skinSelector = new FileSelector(); private final FileSelector capeSelector = new FileSelector(); @@ -93,7 +93,7 @@ public OfflineAccountSkinPane(OfflineAccount account) { Path skin = e.getDragboard().getFiles().get(0).toPath(); Platform.runLater(() -> { skinSelector.setValue(FileUtils.getAbsolutePath(skin)); - skinItem.setSelectedData(Skin.Type.LOCAL_FILE); + skinItem.setSelectedData(OfflineSkinConfig.Type.LOCAL_FILE); }); } }); @@ -109,17 +109,17 @@ public OfflineAccountSkinPane(OfflineAccount account) { layout.setBody(pane); skinItem.loadChildren(Arrays.asList( - new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT), - new MultiFileItem.Option<>(i18n("account.skin.type.steve"), Skin.Type.STEVE), - new MultiFileItem.Option<>(i18n("account.skin.type.alex"), Skin.Type.ALEX), - new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE) + new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), + new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), + new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), + new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE) )); modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName))); modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM); if (account.getSkin() == null) { - skinItem.setSelectedData(Skin.Type.DEFAULT); + skinItem.setSelectedData(OfflineSkinConfig.Type.DEFAULT); modelCombobox.setValue(TextureModel.WIDE); } else { skinItem.setSelectedData(account.getSkin().type()); @@ -194,12 +194,12 @@ public OfflineAccountSkinPane(OfflineAccount account) { layout.setActions(littleSkinLink, acceptButton, cancelButton); } - private Skin getSkin() { - Skin.Type type = skinItem.getSelectedData(); - if (type == Skin.Type.LOCAL_FILE) { - return new Skin(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); + private OfflineSkinConfig getSkin() { + OfflineSkinConfig.Type type = skinItem.getSelectedData(); + if (type == OfflineSkinConfig.Type.LOCAL_FILE) { + return new OfflineSkinConfig(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); } else { - return new Skin(type, null, null, null); + return new OfflineSkinConfig(type, null, null, null); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index 70bcd486c3e..cbf3912e697 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -53,9 +53,9 @@ public class OfflineAccount extends Account { private final AuthlibInjectorArtifactProvider downloader; private final String username; private final UUID uuid; - private Skin skin; + private OfflineSkinConfig skin; - protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Skin skin) { + protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, OfflineSkinConfig skin) { this.downloader = requireNonNull(downloader); this.username = requireNonNull(username); this.uuid = requireNonNull(uuid); @@ -90,17 +90,17 @@ public String getIdentifier() { return username + ":" + username; } - public Skin getSkin() { + public OfflineSkinConfig getSkin() { return skin; } - public void setSkin(Skin skin) { + public void setSkin(OfflineSkinConfig skin) { this.skin = skin; invalidate(); } - protected boolean loadAuthlibInjector(Skin skin) { - return skin != null && skin.type() != Skin.Type.DEFAULT; + protected boolean loadAuthlibInjector(OfflineSkinConfig skin) { + return skin != null && skin.type() != OfflineSkinConfig.Type.DEFAULT; } public AuthInfo logInWithoutSkin() throws AuthenticationException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java index e06dbdbb9e6..634b4f09f81 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java @@ -52,7 +52,7 @@ public OfflineAccount create(String username, UUID uuid) { public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) { AdditionalData data; UUID uuid; - Skin skin; + OfflineSkinConfig skin; if (additionalData != null) { data = (AdditionalData) additionalData; uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid; @@ -71,7 +71,7 @@ public OfflineAccount fromStorage(Map storage) { UUID uuid = tryCast(storage.get("uuid"), String.class) .map(UUIDTypeAdapter::fromString) .orElse(getUUIDFromUserName(username)); - Skin skin = Skin.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null)); + OfflineSkinConfig skin = OfflineSkinConfig.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null)); return new OfflineAccount(downloader, username, uuid, skin); } @@ -82,9 +82,9 @@ public static UUID getUUIDFromUserName(String username) { public static class AdditionalData { private final UUID uuid; - private final Skin skin; + private final OfflineSkinConfig skin; - public AdditionalData(UUID uuid, Skin skin) { + public AdditionalData(UUID uuid, OfflineSkinConfig skin) { this.uuid = uuid; this.skin = skin; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java similarity index 93% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java index f34532a5de0..9623924db99 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java @@ -32,7 +32,7 @@ import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Pair.pair; -public record Skin(Type type, TextureModel textureModel, String localSkinPath, String localCapePath) { +public record OfflineSkinConfig(Type type, TextureModel textureModel, String localSkinPath, String localCapePath) { public enum Type { DEFAULT, @@ -114,7 +114,7 @@ public Task load() { ); } - public static Skin fromStorage(Map storage) { + public static OfflineSkinConfig fromStorage(Map storage) { if (storage == null) return null; Type type = tryCast(storage.get("type"), String.class).flatMap(t -> Optional.ofNullable(Type.fromStorage(t))) @@ -123,7 +123,7 @@ public static Skin fromStorage(Map storage) { String localSkinPath = tryCast(storage.get("localSkinPath"), String.class).orElse(null); String localCapePath = tryCast(storage.get("localCapePath"), String.class).orElse(null); - return new Skin(type, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath); + return new OfflineSkinConfig(type, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath); } public record LoadedSkin(TextureModel model, Texture skin, Texture cape) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 0e20017fadf..d181ca0df8a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -155,9 +155,9 @@ public void addCharacter(Character character) { public static class Character { private final UUID uuid; private final String name; - private final Skin.LoadedSkin skin; + private final OfflineSkinConfig.LoadedSkin skin; - public Character(UUID uuid, String name, Skin.LoadedSkin skin) { + public Character(UUID uuid, String name, OfflineSkinConfig.LoadedSkin skin) { this.uuid = uuid; this.name = name; this.skin = skin; From f5f4d05b30f459dcb9b23cb51d590cbb524fe375 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 10:00:04 +0800 Subject: [PATCH 04/24] =?UTF-8?q?=E6=BA=90=E6=B5=81=E4=B8=87=E4=B8=96=20?= =?UTF-8?q?=E5=A4=A7=E5=93=89=E4=B9=BE=E5=85=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 28 +++++++++++-------- .../hmcl/ui/account/AccountListItem.java | 4 +-- .../ui/account/OfflineAccountSkinPane.java | 2 +- .../java/org/jackhuang/hmcl/auth/Account.java | 4 +-- .../AuthlibInjectorAccount.java | 3 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 4 +-- .../hmcl/auth/microsoft/MicrosoftService.java | 4 +-- .../hmcl/auth/offline/OfflineAccount.java | 4 +-- .../hmcl/auth/offline/OfflineSkinConfig.java | 2 +- .../hmcl/auth/offline/YggdrasilServer.java | 2 +- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 7 ++++- .../hmcl/auth/yggdrasil/YggdrasilService.java | 4 ++- .../yggdrasil => game/skin}/Texture.java | 25 ++--------------- .../yggdrasil => game/skin}/TextureModel.java | 4 +-- .../yggdrasil => game/skin}/TextureType.java | 4 +-- 15 files changed, 47 insertions(+), 54 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{auth/yggdrasil => game/skin}/Texture.java (62%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{auth/yggdrasil => game/skin}/TextureModel.java (87%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{auth/yggdrasil => game/skin}/TextureType.java (85%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index 7f77bad267d..0de26b43e03 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -32,7 +32,11 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; -import org.jackhuang.hmcl.auth.yggdrasil.*; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureModel; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.FXUtils; @@ -79,7 +83,7 @@ public LoadedTexture(Image image, Map metadata) { private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins"); private static Path getTexturePath(Texture texture) { - String url = texture.getUrl(); + String url = texture.url(); int slash = url.lastIndexOf('/'); int dot = url.lastIndexOf('.'); if (dot < slash) { @@ -91,7 +95,7 @@ private static Path getTexturePath(Texture texture) { } public static LoadedTexture loadTexture(Texture texture) throws Throwable { - if (StringUtils.isBlank(texture.getUrl())) { + if (StringUtils.isBlank(texture.url())) { throw new IOException("Texture url is empty"); } @@ -99,14 +103,14 @@ public static LoadedTexture loadTexture(Texture texture) throws Throwable { if (!Files.isRegularFile(file)) { // download it try { - new FileDownloadTask(texture.getUrl(), file).run(); - LOG.info("Texture downloaded: " + texture.getUrl()); + new FileDownloadTask(texture.url(), file).run(); + LOG.info("Texture downloaded: " + texture.url()); } catch (Exception e) { if (Files.isRegularFile(file)) { // concurrency conflict? - LOG.warning("Failed to download texture " + texture.getUrl() + ", but the file is available", e); + LOG.warning("Failed to download texture " + texture.url() + ", but the file is available", e); } else { - throw new IOException("Failed to download texture " + texture.getUrl()); + throw new IOException("Failed to download texture " + texture.url()); } } } @@ -119,7 +123,7 @@ public static LoadedTexture loadTexture(Texture texture) throws Throwable { if (img.isError()) throw img.getException(); - Map metadata = texture.getMetadata(); + Map metadata = texture.metadata(); if (metadata == null) { metadata = emptyMap(); } @@ -168,7 +172,7 @@ public static ObjectBinding skinBinding(YggdrasilService service, } }) .flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))) - .filter(it -> StringUtils.isNotBlank(it.getUrl()))) + .filter(it -> StringUtils.isNotBlank(it.url()))) .asyncMap(it -> { if (it.isPresent()) { Texture texture = it.get(); @@ -176,7 +180,7 @@ public static ObjectBinding skinBinding(YggdrasilService service, try { return loadTexture(texture); } catch (Throwable e) { - LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e); + LOG.warning("Failed to load texture " + texture.url() + ", using fallback texture", e); return uuidFallback; } }, POOL); @@ -224,12 +228,12 @@ public static ObservableValue skinBinding(Account account) { .asyncMap(textures -> { if (textures.isPresent()) { Texture texture = textures.get().get(TextureType.SKIN); - if (texture != null && StringUtils.isNotBlank(texture.getUrl())) { + if (texture != null && StringUtils.isNotBlank(texture.url())) { return CompletableFuture.supplyAsync(() -> { try { return loadTexture(texture); } catch (Throwable e) { - LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e); + LOG.warning("Failed to load texture " + texture.url() + ", using fallback texture", e); return uuidFallback; } }, POOL); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index fa5de7480a4..a5e37791db6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -34,7 +34,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -55,8 +55,8 @@ import static java.util.Collections.emptySet; import static javafx.beans.binding.Bindings.createBooleanBinding; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class AccountListItem extends RadioButton { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index b45ead91c4a..0a45a7d815b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -29,8 +29,8 @@ import javafx.scene.layout.*; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; -import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.game.TexturesLoader; +import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index 3d40c983fe1..348e6ba1ffb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -25,8 +25,8 @@ import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import org.jackhuang.hmcl.auth.yggdrasil.Texture; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.javafx.ObservableHelper; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java index 6be37269aba..99e05bce8d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java @@ -19,13 +19,14 @@ import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.LaunchOptions; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.function.ExceptionalSupplier; + import java.io.IOException; import java.util.*; import java.util.concurrent.CompletableFuture; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index aa061380530..f58b7dd574a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -19,9 +19,9 @@ import javafx.beans.binding.ObjectBinding; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.auth.yggdrasil.Texture; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.nio.file.Path; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java index 883d2d0b749..964b3d0b7ce 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java @@ -27,8 +27,8 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; -import org.jackhuang.hmcl.auth.yggdrasil.Texture; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index cbf3912e697..880e8eb4c1f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -24,10 +24,10 @@ 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.Texture; -import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.LaunchOptions; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java index 9623924db99..e68c077742a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.auth.offline; import javafx.scene.image.Image; -import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; +import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index d181ca0df8a..93756df09ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -19,7 +19,7 @@ import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; -import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; +import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.util.KeyUtils; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index f253eb40b37..ac6dca85e0a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -19,11 +19,16 @@ import javafx.beans.binding.ObjectBinding; import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.nio.file.Path; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.logging.Logger.LOG; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index edbad40a8ad..1cacef86e33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -23,6 +23,8 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; @@ -45,8 +47,8 @@ import static java.util.Collections.unmodifiableList; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.threadPool; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class YggdrasilService { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java similarity index 62% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java index 8542b471b35..7db7fcb6282 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.game.skin; import org.jackhuang.hmcl.util.Immutable; import org.jetbrains.annotations.Nullable; @@ -23,27 +23,8 @@ import java.util.Map; @Immutable -public final class Texture { - - private final String url; - private final Map metadata; - +public record Texture(@Nullable String url, @Nullable Map metadata) { public Texture() { this(null, null); } - - public Texture(String url, Map metadata) { - this.url = url; - this.metadata = metadata; - } - - @Nullable - public String getUrl() { - return url; - } - - @Nullable - public Map getMetadata() { - return metadata; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java similarity index 87% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java index eb9545a90ed..0b852272e96 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.game.skin; public enum TextureModel { WIDE("default"), SLIM("slim"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureType.java similarity index 85% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureType.java index db100eeaaea..1e0dc5afbaf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureType.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.game.skin; public enum TextureType { SKIN, CAPE From 32b0b77a9a20db5cb37a5ee6680f248de64263e9 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 10:05:52 +0800 Subject: [PATCH 05/24] =?UTF-8?q?=E4=B8=8D=E6=9B=BE=E9=97=BB=E6=97=A5?= =?UTF-8?q?=E6=9C=88=E4=BA=89=E8=BE=89=20=E5=9D=8E=E7=A6=BB=E5=A4=8D?= =?UTF-8?q?=E5=BE=80=E7=AB=8B=E4=B8=8B=E6=81=92=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/auth/offline/OfflineSkinConfig.java | 3 +-- .../hmcl/auth/offline/YggdrasilServer.java | 5 ++-- .../jackhuang/hmcl/game/skin/LoadedSkin.java | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java index e68c077742a..85067983f19 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.auth.offline; import javafx.scene.image.Image; +import org.jackhuang.hmcl.game.skin.LoadedSkin; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; @@ -126,6 +127,4 @@ public static OfflineSkinConfig fromStorage(Map storage) { return new OfflineSkinConfig(type, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath); } - public record LoadedSkin(TextureModel model, Texture skin, Texture cape) { - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 93756df09ea..e510ca3dc98 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -19,6 +19,7 @@ import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; +import org.jackhuang.hmcl.game.skin.LoadedSkin; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.util.KeyUtils; import org.jackhuang.hmcl.util.Lang; @@ -155,9 +156,9 @@ public void addCharacter(Character character) { public static class Character { private final UUID uuid; private final String name; - private final OfflineSkinConfig.LoadedSkin skin; + private final LoadedSkin skin; - public Character(UUID uuid, String name, OfflineSkinConfig.LoadedSkin skin) { + public Character(UUID uuid, String name, LoadedSkin skin) { this.uuid = uuid; this.name = name; this.skin = skin; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java new file mode 100644 index 00000000000..0dc97050f31 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java @@ -0,0 +1,23 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game.skin; + +import org.jackhuang.hmcl.auth.offline.Texture; + +public record LoadedSkin(TextureModel model, org.jackhuang.hmcl.auth.offline.Texture skin, Texture cape) { +} From bfb87199a033ddd5a98851d7880839a88cab177e Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 10:15:36 +0800 Subject: [PATCH 06/24] =?UTF-8?q?=E7=85=A7=E4=B8=9C=E5=8D=97=E6=9C=89?= =?UTF-8?q?=E5=9D=A4=E5=BE=87=E4=B9=BE=20=E6=89=BF=E8=A5=BF=E5=8C=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 6 ++--- .../ui/account/OfflineAccountSkinPane.java | 4 +-- .../java/org/jackhuang/hmcl/auth/Account.java | 2 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 2 +- .../hmcl/auth/microsoft/MicrosoftService.java | 2 +- .../{Texture.java => HashedTexture.java} | 27 ++++++------------- .../skin => auth/offline}/LoadedSkin.java | 6 ++--- .../hmcl/auth/offline/OfflineAccount.java | 2 +- .../hmcl/auth/offline/OfflineSkinConfig.java | 9 +++---- .../hmcl/auth/offline/YggdrasilServer.java | 11 ++++---- .../skin => auth/yggdrasil}/Texture.java | 2 +- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 1 - .../hmcl/auth/yggdrasil/YggdrasilService.java | 1 - 13 files changed, 30 insertions(+), 45 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/{Texture.java => HashedTexture.java} (84%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{game/skin => auth/offline}/LoadedSkin.java (79%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{game/skin => auth/yggdrasil}/Texture.java (95%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index 0de26b43e03..6d8f4c2babe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -32,9 +32,9 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; -import org.jackhuang.hmcl.game.skin.Texture; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -203,7 +203,7 @@ public static ObservableValue skinBinding(Account account) { skin.load().setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to load texture", exception); - } else if (result != null && result.skin() != null && result.skin().getImage() != null) { + } else if (result != null && result.skin() != null && result.skin().image() != null) { Map metadata; if (result.model() != null) { metadata = singletonMap("model", result.model().modelName); @@ -211,7 +211,7 @@ public static ObservableValue skinBinding(Account account) { metadata = emptyMap(); } - binding.set(new LoadedTexture(result.skin().getImage(), metadata)); + binding.set(new LoadedTexture(result.skin().image(), metadata)); } }).start(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index 0a45a7d815b..ff8906df21d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -145,9 +145,9 @@ public OfflineAccountSkinPane(OfflineAccount account) { return; } canvas.updateSkin( - result.skin() != null ? result.skin().getImage() : TexturesLoader.getDefaultSkin(uuid).image(), + result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).image(), result.model() == TextureModel.SLIM, - result.cape() != null ? result.cape().getImage() : null); + result.cape() != null ? result.cape().image() : null); } }).start(); }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index 348e6ba1ffb..38753913bca 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -25,7 +25,7 @@ import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.javafx.ObservableHelper; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index f58b7dd574a..878a5ae0d90 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -19,8 +19,8 @@ import javafx.beans.binding.ObjectBinding; import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; -import org.jackhuang.hmcl.game.skin.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.javafx.BindingMapping; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java index 964b3d0b7ce..ec59385fb36 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java @@ -27,7 +27,7 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; -import org.jackhuang.hmcl.game.skin.Texture; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Texture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/HashedTexture.java similarity index 84% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Texture.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/HashedTexture.java index be9690f3090..55232f71a0b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Texture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/HashedTexture.java @@ -30,30 +30,19 @@ import static java.util.Objects.requireNonNull; -public final class Texture { - private final String hash; - private final Image image; - - public Texture(String hash, Image image) { +public record HashedTexture(String hash, Image image) { + public HashedTexture(String hash, Image image) { this.hash = requireNonNull(hash); this.image = requireNonNull(image); } - public String getHash() { - return hash; - } - - public Image getImage() { - return image; - } - - private static final Map textures = new HashMap<>(); + private static final Map textures = new HashMap<>(); public static boolean hasTexture(String hash) { return textures.containsKey(hash); } - public static Texture getTexture(String hash) { + public static HashedTexture getTexture(String hash) { return textures.get(hash); } @@ -100,7 +89,7 @@ private static void putInt(byte[] array, int offset, int x) { array[offset + 3] = (byte) (x >> 0 & 0xff); } - public static Texture loadTexture(InputStream in) throws IOException { + public static HashedTexture loadTexture(InputStream in) throws IOException { if (in == null) return null; Image img; try (InputStream is = in) { @@ -113,17 +102,17 @@ public static Texture loadTexture(InputStream in) throws IOException { return loadTexture(img); } - public static Texture loadTexture(Image image) { + public static HashedTexture loadTexture(Image image) { if (image == null) return null; String hash = computeTextureHash(image); - Texture existent = textures.get(hash); + HashedTexture existent = textures.get(hash); if (existent != null) { return existent; } - Texture texture = new Texture(hash, image); + HashedTexture texture = new HashedTexture(hash, image); existent = textures.putIfAbsent(hash, texture); if (existent != null) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java similarity index 79% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java index 0dc97050f31..6e575541df2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.game.skin; +package org.jackhuang.hmcl.auth.offline; -import org.jackhuang.hmcl.auth.offline.Texture; +import org.jackhuang.hmcl.game.skin.TextureModel; -public record LoadedSkin(TextureModel model, org.jackhuang.hmcl.auth.offline.Texture skin, Texture cape) { +public record LoadedSkin(TextureModel model, HashedTexture skin, HashedTexture cape) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index 880e8eb4c1f..b8cb924fa52 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -24,9 +24,9 @@ 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.Texture; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.LaunchOptions; -import org.jackhuang.hmcl.game.skin.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.ToStringBuilder; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java index 85067983f19..4062e32abfa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.auth.offline; import javafx.scene.image.Image; -import org.jackhuang.hmcl.game.skin.LoadedSkin; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; @@ -89,16 +88,16 @@ public Task load() { return Task.supplyAsync(() -> new LoadedSkin( model, - Texture.loadTexture(new Image(resource)), + HashedTexture.loadTexture(new Image(resource)), null )); case LOCAL_FILE: return Task.supplyAsync(() -> { - Texture skin = null, cape = null; + HashedTexture skin = null, cape = null; Optional skinPath = FileUtils.tryGetPath(localSkinPath); Optional capePath = FileUtils.tryGetPath(localCapePath); - if (skinPath.isPresent()) skin = Texture.loadTexture(Files.newInputStream(skinPath.get())); - if (capePath.isPresent()) cape = Texture.loadTexture(Files.newInputStream(capePath.get())); + if (skinPath.isPresent()) skin = HashedTexture.loadTexture(Files.newInputStream(skinPath.get())); + if (capePath.isPresent()) cape = HashedTexture.loadTexture(Files.newInputStream(capePath.get())); return new LoadedSkin(textureModel(), skin, cape); }); default: diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index e510ca3dc98..1a64f2a9e97 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -19,7 +19,6 @@ import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; -import org.jackhuang.hmcl.game.skin.LoadedSkin; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.util.KeyUtils; import org.jackhuang.hmcl.util.Lang; @@ -128,9 +127,9 @@ private Response profile(Request request) { private Response texture(Request request) { String hash = request.getPathVariables().group("hash"); - if (Texture.hasTexture(hash)) { - Texture texture = Texture.getTexture(hash); - byte[] data = PNGJavaFXUtils.writeImageToArray(texture.getImage()); + if (HashedTexture.hasTexture(hash)) { + HashedTexture texture = HashedTexture.getTexture(hash); + byte[] data = PNGJavaFXUtils.writeImageToArray(texture.image()); Response response = newFixedLengthResponse(Response.Status.OK, "image/png", new ByteArrayInputStream(data), data.length); response.addHeader("Etag", String.format("\"%s\"", hash)); response.addHeader("Cache-Control", "max-age=2592000, public"); @@ -179,7 +178,7 @@ public GameProfile toSimpleResponse() { public Object toCompleteResponse(String rootUrl) { Map realTextures = new HashMap<>(); if (skin != null && skin.skin() != null) { - String url = rootUrl + "/textures/" + skin.skin().getHash(); + String url = rootUrl + "/textures/" + skin.skin().hash(); if (skin.model() == TextureModel.SLIM) { realTextures.put("SKIN", mapOf( pair("url", url), @@ -191,7 +190,7 @@ public Object toCompleteResponse(String rootUrl) { } } if (skin != null && skin.cape() != null) { - realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.cape().getHash()))); + realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.cape().hash()))); } Map textureResponse = mapOf( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java similarity index 95% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java index 7db7fcb6282..8f6534ff203 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Texture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.game.skin; +package org.jackhuang.hmcl.auth.yggdrasil; import org.jackhuang.hmcl.util.Immutable; import org.jetbrains.annotations.Nullable; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index ac6dca85e0a..f2b15bc33ae 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -19,7 +19,6 @@ import javafx.beans.binding.ObjectBinding; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.game.skin.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.javafx.BindingMapping; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index 1cacef86e33..c4684499902 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -23,7 +23,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; -import org.jackhuang.hmcl.game.skin.Texture; import org.jackhuang.hmcl.game.skin.TextureType; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; From ca93495b4f0d4c517b9ca01f6be08f896e7e5c46 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 10:21:13 +0800 Subject: [PATCH 07/24] =?UTF-8?q?=E5=A4=A9=E9=81=93=E8=87=AA=E6=98=86?= =?UTF-8?q?=E4=BB=91=E5=B7=8D=E5=B7=8D=20=E7=BF=BB=E8=B5=B7=E5=8D=8E?= =?UTF-8?q?=E5=A4=8F=E5=B7=BD=E9=9C=87=E8=89=AE=E5=85=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/auth/offline/LoadedOfflineSkin.java | 23 +++++++++++++++ .../hmcl/auth/offline/OfflineSkinConfig.java | 6 ++-- .../hmcl/auth/offline/YggdrasilServer.java | 24 ++-------------- .../jackhuang/hmcl/auth/yggdrasil/User.java | 28 +++---------------- .../hmcl/auth/yggdrasil/YggdrasilService.java | 2 +- .../offline => game/skin}/LoadedSkin.java | 6 ++-- 6 files changed, 37 insertions(+), 52 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedOfflineSkin.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/{auth/offline => game/skin}/LoadedSkin.java (81%) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedOfflineSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedOfflineSkin.java new file mode 100644 index 00000000000..30fbdf77be5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedOfflineSkin.java @@ -0,0 +1,23 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.auth.offline; + +import org.jackhuang.hmcl.game.skin.TextureModel; + +public record LoadedOfflineSkin(TextureModel model, HashedTexture skin, HashedTexture cape) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java index 4062e32abfa..5111586e8e7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineSkinConfig.java @@ -70,7 +70,7 @@ public TextureModel textureModel() { return textureModel == null ? TextureModel.WIDE : textureModel; } - public Task load() { + public Task load() { switch (type) { case DEFAULT: return Task.supplyAsync(() -> null); @@ -86,7 +86,7 @@ public Task load() { TextureModel model = this.textureModel != null ? this.textureModel : type == Type.ALEX ? TextureModel.SLIM : TextureModel.WIDE; String resource = (model == TextureModel.SLIM ? "/assets/img/skin/slim/" : "/assets/img/skin/wide/") + type.name().toLowerCase(Locale.ROOT) + ".png"; - return Task.supplyAsync(() -> new LoadedSkin( + return Task.supplyAsync(() -> new LoadedOfflineSkin( model, HashedTexture.loadTexture(new Image(resource)), null @@ -98,7 +98,7 @@ public Task load() { Optional capePath = FileUtils.tryGetPath(localCapePath); if (skinPath.isPresent()) skin = HashedTexture.loadTexture(Files.newInputStream(skinPath.get())); if (capePath.isPresent()) cape = HashedTexture.loadTexture(Files.newInputStream(capePath.get())); - return new LoadedSkin(textureModel(), skin, cape); + return new LoadedOfflineSkin(textureModel(), skin, cape); }); default: throw new UnsupportedOperationException(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 1a64f2a9e97..3eaf69ff7f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -148,29 +148,11 @@ private Optional findCharacterByName(String uuid) { } public void addCharacter(Character character) { - charactersByUuid.put(character.getUUID(), character); - charactersByName.put(character.getName(), character); + charactersByUuid.put(character.uuid(), character); + charactersByName.put(character.name(), character); } - public static class Character { - private final UUID uuid; - private final String name; - private final LoadedSkin skin; - - public Character(UUID uuid, String name, LoadedSkin skin) { - this.uuid = uuid; - this.name = name; - this.skin = skin; - } - - public UUID getUUID() { - return uuid; - } - - public String getName() { - return name; - } - + public record Character(UUID uuid, String name, LoadedOfflineSkin skin) { public GameProfile toSimpleResponse() { return new GameProfile(uuid, name); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java index f3c09d8775a..a7da58c0391 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java @@ -18,46 +18,26 @@ package org.jackhuang.hmcl.auth.yggdrasil; import com.google.gson.JsonParseException; - -import java.util.Map; - import com.google.gson.annotations.JsonAdapter; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.Validation; import org.jetbrains.annotations.Nullable; +import java.util.Map; + /** * * @author huang */ @Immutable -public final class User implements Validation { - - private final String id; - - @Nullable - @JsonAdapter(PropertyMapSerializer.class) - private final Map properties; +public record User(String id, + @JsonAdapter(PropertyMapSerializer.class) @Nullable Map properties) implements Validation { public User(String id) { this(id, null); } - public User(String id, @Nullable Map properties) { - this.id = id; - this.properties = properties; - } - - public String getId() { - return id; - } - - @Nullable - public Map getProperties() { - return properties; - } - @Override public void validate() throws JsonParseException { if (StringUtils.isBlank(id)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index c4684499902..5234c78afe6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -212,7 +212,7 @@ private static YggdrasilSession handleAuthenticationResponse(String responseText response.accessToken, response.selectedProfile, response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles), - response.user == null ? null : response.user.getProperties()); + response.user == null ? null : response.user.properties()); } private static void requireEmpty(String response) throws AuthenticationException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java similarity index 81% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java index 6e575541df2..8d1dfe978ac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/LoadedSkin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.auth.offline; +package org.jackhuang.hmcl.game.skin; -import org.jackhuang.hmcl.game.skin.TextureModel; +import javafx.scene.image.Image; -public record LoadedSkin(TextureModel model, HashedTexture skin, HashedTexture cape) { +public record LoadedSkin(TextureModel model, Image skin, Image cape) { } From 6de972d3157778a9f71d87b92eb63d03b26cdc6e Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 10:59:25 +0800 Subject: [PATCH 08/24] =?UTF-8?q?=E4=B8=87=E8=B1=A1=E4=BA=88=E4=B8=87?= =?UTF-8?q?=E7=81=B5=E5=BE=97=E8=A7=81=20=E4=B8=A4=E7=9B=B8=E7=9B=88?= =?UTF-8?q?=E5=B2=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 2 + .../ui/account/skin/GameSkinPageBase.java | 92 +++++++++++++++++++ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../game/skin/{LoadedSkin.java => Skin.java} | 2 +- 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/{LoadedSkin.java => Skin.java} (92%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 13a67943c16..9a3603f50c1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -38,6 +38,7 @@ public enum SVG { ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"), ARROW_DROP_UP("M7 14 12 9 17 14H7Z"), ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"), + APPAREL("m6 10.95l-1 .55q-.35.2-.75.1t-.6-.45l-2-3.5q-.2-.35-.1-.75T2 6.3L7.75 3H9.5q.225 0 .363.138T10 3.5V4q0 .825.588 1.413T12 6t1.413-.587T14 4v-.5q0-.225.138-.363T14.5 3h1.75L22 6.3q.35.2.45.6t-.1.75l-2 3.5q-.2.35-.588.438T19 11.475l-1-.5V20q0 .425-.288.713T17 21H7q-.425 0-.712-.288T6 20z"), BETA_CIRCLE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"), @@ -49,6 +50,7 @@ public enum SVG { CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"), CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"), CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"), + CROP_9_16("M9 21q-.825 0-1.412-.587T7 19V5q0-.825.588-1.412T9 3h6q.825 0 1.413.588T17 5v14q0 .825-.587 1.413T15 21z"), DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"), DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"), DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java new file mode 100644 index 00000000000..216c885dd19 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java @@ -0,0 +1,92 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.account.skin; + +import com.jfoenix.controls.JFXPopup; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.geometry.Insets; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.game.skin.Skin; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.versions.VersionSettingsPage; + +import java.util.Map; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public abstract class GameSkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { + protected final Account account; + private final Map urls; + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); + private final TabHeader tab; + private final TabHeader.Tab manageTab = new TabHeader.Tab<>("manageTab"); + private final TransitionPane transitionPane = new TransitionPane(); + + public GameSkinPageBase(Account account, Map urls) { + this.urls = urls; + this.account = account; + + tab = new TabHeader(transitionPane, manageTab); + tab.select(manageTab); + + BorderPane left = new BorderPane(); + FXUtils.setLimitWidth(left, 200); + VBox.setVgrow(left, Priority.ALWAYS); + setLeft(left); + + AdvancedListBox sideBar = new AdvancedListBox().addNavigationDrawerTab(tab, manageTab, i18n("account.skin.manage"), SVG.CHECKROOM); + left.setTop(sideBar); + + PopupMenu saveList = new PopupMenu(); + JFXPopup savePopup = new JFXPopup(saveList); + saveList.getContent().setAll( + new IconedMenuItem(SVG.APPAREL, i18n("account.skin"), ()->{}, savePopup), + new IconedMenuItem(SVG.CROP_9_16, i18n("version.launch_script"), ()->{}, savePopup) + ); + + AdvancedListBox toolbar = new AdvancedListBox() + .addNavigationDrawerItem(i18n("go"), SVG.OUTPUT, () -> { + + }); + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); + left.setBottom(toolbar); + + setCenter(transitionPane); + + this.state.set(State.fromTitle(i18n("account.skin.manage", account.getIdentifier()))); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } + + protected abstract ReadOnlyObjectProperty skinObjectProperty(); + + protected abstract Task uploadSkin(Skin skin); +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index e13e61bd67b..40dad8a7f80 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -150,6 +150,7 @@ account.not_logged_in=Not Logged in account.password=Password account.portable=Portable account.skin=Skin +account.skin.manage=Skin Management - %1s account.skin.file=Skin File account.skin.model=Model account.skin.model.default=Classic diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 555f135af8d..64004e4bc1f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -148,6 +148,7 @@ account.not_logged_in=未登入 account.password=密碼 account.portable=可攜式帳戶 account.skin=外觀 +account.skin.manage=外觀管理 - %1s account.skin.file=外觀圖片檔案 account.skin.model=模型 account.skin.model.default=寬型 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index b251db3d68a..7bbaf5bad28 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -150,6 +150,7 @@ account.not_logged_in=未登录 account.password=密码 account.portable=便携账户 account.skin=皮肤 +account.skin.manage=皮肤管理 - %1s account.skin.file=皮肤图片文件 account.skin.model=模型 account.skin.model.default=宽型 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java similarity index 92% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java index 8d1dfe978ac..33cb03bb8f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/LoadedSkin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java @@ -19,5 +19,5 @@ import javafx.scene.image.Image; -public record LoadedSkin(TextureModel model, Image skin, Image cape) { +public record Skin(TextureModel model, Image skin, Image cape) { } From 9ebb8dffbb6e33a0a0e766d3b89da393dcd12546 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 14:14:33 +0800 Subject: [PATCH 09/24] =?UTF-8?q?=E6=BD=9C=E9=BE=99=E9=95=BF=E7=94=9F?= =?UTF-8?q?=E5=BA=94=E7=B4=AB=E5=BE=AE=20=E6=83=9F=E5=90=91=E5=9B=9B?= =?UTF-8?q?=E6=96=B9=E4=BA=94=E6=B0=94=E5=AF=BB=E9=81=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 4 +- .../hmcl/ui/account/AccountListItem.java | 3 +- .../account/skin/OfflineAccountSkinPage.java | 39 +++++++++++ ...ameSkinPageBase.java => SkinPageBase.java} | 68 +++++++++++++------ .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + .../org/jackhuang/hmcl/game/skin/Skin.java | 7 +- .../hmcl/game/skin/TextureModel.java | 4 ++ 9 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/{GameSkinPageBase.java => SkinPageBase.java} (50%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 9a3603f50c1..d704425fd23 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -38,7 +38,7 @@ public enum SVG { ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"), ARROW_DROP_UP("M7 14 12 9 17 14H7Z"), ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"), - APPAREL("m6 10.95l-1 .55q-.35.2-.75.1t-.6-.45l-2-3.5q-.2-.35-.1-.75T2 6.3L7.75 3H9.5q.225 0 .363.138T10 3.5V4q0 .825.588 1.413T12 6t1.413-.587T14 4v-.5q0-.225.138-.363T14.5 3h1.75L22 6.3q.35.2.45.6t-.1.75l-2 3.5q-.2.35-.588.438T19 11.475l-1-.5V20q0 .425-.288.713T17 21H7q-.425 0-.712-.288T6 20z"), + APPAREL("m6 10.95l-1.875 1.025l-2.975-5.2L7.75 3H10v1q0 .825.588 1.413T12 6t1.413-.587T14 4V3h2.25l6.6 3.775l-2.95 5.15l-1.9-.95V21H6zM8 7.6V19h8V7.6l3.1 1.7l1.05-1.75l-4.3-2.5q-.375 1.275-1.412 2.113T12 8t-2.437-.837T8.15 5.05l-4.3 2.5L4.9 9.3zm4 4.425"), BETA_CIRCLE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"), @@ -50,7 +50,7 @@ public enum SVG { CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"), CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"), CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"), - CROP_9_16("M9 21q-.825 0-1.412-.587T7 19V5q0-.825.588-1.412T9 3h6q.825 0 1.413.588T17 5v14q0 .825-.587 1.413T15 21z"), + CROP_9_16("M9 21q-.825 0-1.412-.587T7 19V5q0-.825.588-1.412T9 3h6q.825 0 1.413.588T17 5v14q0 .825-.587 1.413T15 21zM9 5v14h6V5zm0 0v14z"), DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"), DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"), DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index a5e37791db6..0306255930b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -40,6 +40,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.DialogController; +import org.jackhuang.hmcl.ui.account.skin.OfflineAccountSkinPage; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.skin.InvalidSkinException; @@ -138,7 +139,7 @@ public ObservableBooleanValue canUploadSkin() { @Nullable public Task uploadSkin() { if (account instanceof OfflineAccount) { - Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account)); + Controllers.navigate(new OfflineAccountSkinPage((OfflineAccount) account)); return null; } if (!account.canUploadSkin()) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java new file mode 100644 index 00000000000..0a8f1c91fab --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -0,0 +1,39 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.account.skin; + +import javafx.beans.property.ReadOnlyObjectProperty; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.game.skin.Skin; +import org.jackhuang.hmcl.task.Task; + +public class OfflineAccountSkinPage extends SkinPageBase { + public OfflineAccountSkinPage(OfflineAccount account) { + super(account, null); + } + + @Override + protected ReadOnlyObjectProperty skinObjectProperty() { + return null; + } + + @Override + protected Task uploadSkin(Skin skin) { + return null; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java similarity index 50% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 216c885dd19..9ac286facc7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/GameSkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -21,10 +21,9 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Insets; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.skin.Skin; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; @@ -33,25 +32,28 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.versions.VersionSettingsPage; - -import java.util.Map; +import org.jackhuang.hmcl.ui.skin.SkinCanvas; +import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning; +import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms; +import org.jetbrains.annotations.Nullable; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public abstract class GameSkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { +public abstract class SkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { protected final Account account; - private final Map urls; + @Nullable + private final String url; private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final TabHeader tab; - private final TabHeader.Tab manageTab = new TabHeader.Tab<>("manageTab"); + private final TabHeader.Tab manageTab = new TabHeader.Tab<>("manageTab"); private final TransitionPane transitionPane = new TransitionPane(); - public GameSkinPageBase(Account account, Map urls) { - this.urls = urls; + public SkinPageBase(Account account, @Nullable String url) { + this.url = url; this.account = account; tab = new TabHeader(transitionPane, manageTab); + manageTab.setNodeSupplier(Right::new); tab.select(manageTab); BorderPane left = new BorderPane(); @@ -59,26 +61,25 @@ public GameSkinPageBase(Account account, Map urls) { VBox.setVgrow(left, Priority.ALWAYS); setLeft(left); - AdvancedListBox sideBar = new AdvancedListBox().addNavigationDrawerTab(tab, manageTab, i18n("account.skin.manage"), SVG.CHECKROOM); + AdvancedListBox sideBar = new AdvancedListBox().addNavigationDrawerTab(tab, manageTab, i18n("account.skin"), SVG.CHECKROOM); left.setTop(sideBar); PopupMenu saveList = new PopupMenu(); JFXPopup savePopup = new JFXPopup(saveList); - saveList.getContent().setAll( - new IconedMenuItem(SVG.APPAREL, i18n("account.skin"), ()->{}, savePopup), - new IconedMenuItem(SVG.CROP_9_16, i18n("version.launch_script"), ()->{}, savePopup) - ); + saveList.getContent().setAll(new IconedMenuItem(SVG.APPAREL, i18n("account.skin.manage.save.skin"), () -> { - AdvancedListBox toolbar = new AdvancedListBox() - .addNavigationDrawerItem(i18n("go"), SVG.OUTPUT, () -> { + }, savePopup), new IconedMenuItem(SVG.CROP_9_16, i18n("account.skin.manage.save.cape"), () -> { + }, savePopup)); - }); + AdvancedListBox toolbar = new AdvancedListBox().addNavigationDrawerItem(i18n("button.save"), SVG.OUTPUT, null, item -> { + item.setOnAction(e -> savePopup.show(item, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, item.getWidth(), 0)); + }); BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); setCenter(transitionPane); - this.state.set(State.fromTitle(i18n("account.skin.manage", account.getIdentifier()))); + this.state.set(State.fromTitle(i18n("account.skin.manage", account.getUsername()))); } @Override @@ -89,4 +90,31 @@ public ReadOnlyObjectProperty stateProperty() { protected abstract ReadOnlyObjectProperty skinObjectProperty(); protected abstract Task uploadSkin(Skin skin); + + private final class Right extends HBox { + private Right() { + setSpacing(10); + setPadding(new Insets(10, 10, 10, 10)); + + FlowPane leftRegion = new FlowPane(); + leftRegion.getStyleClass().add("card-non-transparent"); + HBox.setHgrow(leftRegion, Priority.ALWAYS); + + BorderPane rightRegion = new BorderPane(); + rightRegion.getStyleClass().add("card-non-transparent"); + FXUtils.setLimitWidth(rightRegion, 250); + + SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 250, 300, true); + skinObjectProperty().addListener((obs, oldSkin, newSkin) -> { + canvas.updateSkin(newSkin.skin().image(), newSkin.model().isSlim(), newSkin.cape() != null ? newSkin.cape().image() : null); + }); + StackPane canvasPane = new StackPane(canvas); + canvasPane.setPrefWidth(300); + rightRegion.setCenter(canvasPane); + canvas.getAnimationPlayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas)); + canvas.enableRotation(.5); + + getChildren().addAll(leftRegion, rightRegion); + } + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 40dad8a7f80..4c8f45382c5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -151,6 +151,8 @@ account.password=Password account.portable=Portable account.skin=Skin account.skin.manage=Skin Management - %1s +account.skin.manage.save.skin=Save Skin +account.skin.manage.save.cape=Save Cape account.skin.file=Skin File account.skin.model=Model account.skin.model.default=Classic diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 64004e4bc1f..69870abe00c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -149,6 +149,8 @@ account.password=密碼 account.portable=可攜式帳戶 account.skin=外觀 account.skin.manage=外觀管理 - %1s +account.skin.manage.save.skin=保存外觀 +account.skin.manage.save.cape=保存披风 account.skin.file=外觀圖片檔案 account.skin.model=模型 account.skin.model.default=寬型 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 7bbaf5bad28..df89f1675dc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -151,6 +151,8 @@ account.password=密码 account.portable=便携账户 account.skin=皮肤 account.skin.manage=皮肤管理 - %1s +account.skin.manage.save.skin=保存皮肤 +account.skin.manage.save.cape=保存披风 account.skin.file=皮肤图片文件 account.skin.model=模型 account.skin.model.default=宽型 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java index 33cb03bb8f4..ff647668f54 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java @@ -18,6 +18,11 @@ package org.jackhuang.hmcl.game.skin; import javafx.scene.image.Image; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public record Skin(TextureModel model, Image skin, Image cape) { +public record Skin(@NotNull TextureModel model, @NotNull TextureObject skin, @Nullable TextureObject cape) { + public record TextureObject(@NotNull Image image, @NotNull String url) { + + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java index 0b852272e96..593a946806e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureModel.java @@ -25,4 +25,8 @@ public enum TextureModel { TextureModel(String modelName) { this.modelName = modelName; } + + public boolean isSlim() { + return modelName.equals("slim"); + } } From 965a7a0282a30530eddde52f388c54fbe0331e79 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 15:53:11 +0800 Subject: [PATCH 10/24] =?UTF-8?q?=E7=87=A7=E7=81=AB=E6=97=81=E5=85=AB?= =?UTF-8?q?=E5=8D=A6=E7=99=BE=E8=8D=89=20=E6=8F=86=E7=BB=8F=E7=BA=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java | 2 +- .../java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 0a8f1c91fab..ba25e609569 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -33,7 +33,7 @@ protected ReadOnlyObjectProperty skinObjectProperty() { } @Override - protected Task uploadSkin(Skin skin) { + protected Task setSkin(Skin skin) { return null; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 9ac286facc7..49e82290eab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -89,7 +89,7 @@ public ReadOnlyObjectProperty stateProperty() { protected abstract ReadOnlyObjectProperty skinObjectProperty(); - protected abstract Task uploadSkin(Skin skin); + protected abstract Task setSkin(Skin skin); private final class Right extends HBox { private Right() { From d66d952558e96319f2dd9a0d3705d4f0ba212f05 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 22:32:48 +0800 Subject: [PATCH 11/24] =?UTF-8?q?=E6=AD=A3=E4=BD=8D=E7=BA=AA=E5=A4=A9?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E5=BD=92=20=E4=B8=8D=E6=B6=88=E7=A5=88?= =?UTF-8?q?=E5=A4=A9=E9=80=80=E6=B0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java index ff8906df21d..32ba56abaae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java @@ -36,7 +36,6 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.FileSelector; -import org.jackhuang.hmcl.ui.construct.JFXHyperlink; import org.jackhuang.hmcl.ui.construct.MultiFileItem; import org.jackhuang.hmcl.ui.skin.SkinCanvas; import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning; @@ -184,14 +183,12 @@ public OfflineAccountSkinPane(OfflineAccount account) { fireEvent(new DialogCloseEvent()); }); - JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin")); - littleSkinLink.setOnAction(e -> FXUtils.openLink("https://littleskin.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); + layout.setActions(acceptButton, cancelButton); } private OfflineSkinConfig getSkin() { From 45922a245b8c369ded0e27707d71bc14f1d61143 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 22:49:32 +0800 Subject: [PATCH 12/24] =?UTF-8?q?=E5=88=9D=E9=9A=BE=E7=9F=A5=E4=B8=80?= =?UTF-8?q?=E5=BF=B5=E4=B8=80=E5=86=B3=E7=94=9F=E9=BE=99=E9=AB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/skin/OfflineAccountSkinPage.java | 124 +++++++++++++++--- .../hmcl/ui/account/skin/SkinPageBase.java | 37 ++++-- .../org/jackhuang/hmcl/game/skin/Skin.java | 4 - .../hmcl/game/skin/TextureObject.java | 25 ++++ 4 files changed, 154 insertions(+), 36 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index ba25e609569..72d3a2e1985 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -1,39 +1,123 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ package org.jackhuang.hmcl.ui.account.skin; +import com.jfoenix.controls.JFXComboBox; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; import org.jackhuang.hmcl.game.skin.Skin; +import org.jackhuang.hmcl.game.skin.TextureModel; +import org.jackhuang.hmcl.game.skin.TextureObject; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.FileSelector; +import org.jackhuang.hmcl.ui.construct.MultiFileItem; + +import java.util.Arrays; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class OfflineAccountSkinPage extends SkinPageBase { + private ReadOnlyObjectWrapper skinProperty; + + private final MultiFileItem skinItem = new MultiFileItem<>(); + private final JFXComboBox modelCombobox = new JFXComboBox<>(); + private final FileSelector skinSelector = new FileSelector(); + private final FileSelector capeSelector = new FileSelector(); -public class OfflineAccountSkinPage extends SkinPageBase { public OfflineAccountSkinPage(OfflineAccount account) { super(account, null); + + skinItem.loadChildren(Arrays.asList( + new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), + new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), + new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), + new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE) + )); + + modelCombobox.setConverter(FXUtils.stringConverter(model -> i18n("account.skin.model." + model.modelName))); + modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM); + + OfflineSkinConfig config = account.getSkin(); + if (config == null) { + skinItem.setSelectedData(OfflineSkinConfig.Type.DEFAULT); + modelCombobox.setValue(TextureModel.WIDE); + } else { + skinItem.setSelectedData(config.type()); + modelCombobox.setValue(config.textureModel() != null ? config.textureModel() : TextureModel.WIDE); + skinSelector.setValue(config.localSkinPath()); + capeSelector.setValue(config.localCapePath()); + } + + StackPane contentPane = super.skinManage.leftRegion; + + VBox settingsBox = new VBox(20); + GridPane grid = new GridPane(); + grid.setAlignment(Pos.CENTER); + grid.setHgap(16); + grid.setVgap(10); + + skinItem.selectedDataProperty().addListener((obs, oldVal, newVal) -> { + grid.getChildren().clear(); + if (newVal == OfflineSkinConfig.Type.LOCAL_FILE) { + grid.addRow(0, new Label(i18n("account.skin.model")), modelCombobox); + grid.addRow(1, new Label(i18n("account.skin")), skinSelector); + grid.addRow(2, new Label(i18n("account.cape")), capeSelector); + } + }); + + settingsBox.getChildren().addAll(skinItem, grid); + contentPane.getChildren().setAll(settingsBox); + StackPane.setAlignment(settingsBox, Pos.CENTER); + settingsBox.setAlignment(Pos.CENTER); + + FXUtils.observeWeak(this::loadSkinPreview, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), + skinSelector.valueProperty(), capeSelector.valueProperty()); + + loadSkinPreview(); + } + + private void loadSkinPreview() { + OfflineSkinConfig config = getConfig(); + config.load().whenComplete(Schedulers.javafx(), (loadedSkin, throwable) -> { + if (throwable == null && loadedSkin != null) { + TextureObject skinTex = loadedSkin.skin() != null + ? new TextureObject(loadedSkin.skin().image(), "") : null; + TextureObject capeTex = loadedSkin.cape() != null + ? new TextureObject(loadedSkin.cape().image(), "") : null; + + if (skinTex != null || capeTex != null) { + skinProperty.set(new Skin(loadedSkin.model(), skinTex, capeTex)); + } + } + }).start(); + } + + private OfflineSkinConfig getConfig() { + OfflineSkinConfig.Type type = skinItem.getSelectedData(); + if (type == OfflineSkinConfig.Type.LOCAL_FILE) { + return new OfflineSkinConfig(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); + } + return new OfflineSkinConfig(type, null, null, null); } @Override protected ReadOnlyObjectProperty skinObjectProperty() { - return null; + if (skinProperty == null) skinProperty = new ReadOnlyObjectWrapper<>(); + return skinProperty.getReadOnlyProperty(); } @Override protected Task setSkin(Skin skin) { - return null; + return Task.supplyAsync(() -> { + account.setSkin(getConfig()); + return null; + }); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 49e82290eab..6b590cd4dd9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -18,13 +18,16 @@ package org.jackhuang.hmcl.ui.account.skin; import com.jfoenix.controls.JFXPopup; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.scene.layout.*; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.skin.Skin; +import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -39,21 +42,25 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public abstract class SkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { - protected final Account account; +public abstract class SkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { + protected final T account; @Nullable private final String url; private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); + private final BooleanProperty loadingProperty = new SimpleBooleanProperty(true); private final TabHeader tab; - private final TabHeader.Tab manageTab = new TabHeader.Tab<>("manageTab"); + private final TabHeader.Tab manageTab = new TabHeader.Tab<>("manageTab"); private final TransitionPane transitionPane = new TransitionPane(); - public SkinPageBase(Account account, @Nullable String url) { + protected final SkinManage skinManage; + + protected SkinPageBase(T account, @Nullable String url) { this.url = url; this.account = account; tab = new TabHeader(transitionPane, manageTab); - manageTab.setNodeSupplier(Right::new); + skinManage = new SkinManage(); + manageTab.setNodeSupplier(() -> skinManage); tab.select(manageTab); BorderPane left = new BorderPane(); @@ -67,7 +74,6 @@ public SkinPageBase(Account account, @Nullable String url) { PopupMenu saveList = new PopupMenu(); JFXPopup savePopup = new JFXPopup(saveList); saveList.getContent().setAll(new IconedMenuItem(SVG.APPAREL, i18n("account.skin.manage.save.skin"), () -> { - }, savePopup), new IconedMenuItem(SVG.CROP_9_16, i18n("account.skin.manage.save.cape"), () -> { }, savePopup)); @@ -91,20 +97,27 @@ public ReadOnlyObjectProperty stateProperty() { protected abstract Task setSkin(Skin skin); - private final class Right extends HBox { - private Right() { + protected final class SkinManage extends HBox { + protected StackPane leftRegion = new StackPane(); + private BorderPane rightRegion = new BorderPane(); + + private SkinManage() { setSpacing(10); setPadding(new Insets(10, 10, 10, 10)); - FlowPane leftRegion = new FlowPane(); leftRegion.getStyleClass().add("card-non-transparent"); HBox.setHgrow(leftRegion, Priority.ALWAYS); - BorderPane rightRegion = new BorderPane(); rightRegion.getStyleClass().add("card-non-transparent"); FXUtils.setLimitWidth(rightRegion, 250); - SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 250, 300, true); + + var uuid = account.getUUID(); + var skin = TexturesLoader.getDefaultSkin(uuid).image(); + var slim = TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM; + + SkinCanvas canvas = new SkinCanvas(skin, 250, 300, true); + canvas.updateSkin(skin, slim, null); skinObjectProperty().addListener((obs, oldSkin, newSkin) -> { canvas.updateSkin(newSkin.skin().image(), newSkin.model().isSlim(), newSkin.cape() != null ? newSkin.cape().image() : null); }); @@ -114,7 +127,7 @@ private Right() { canvas.getAnimationPlayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas)); canvas.enableRotation(.5); - getChildren().addAll(leftRegion, rightRegion); + getChildren().setAll(leftRegion, rightRegion); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java index ff647668f54..0f5e1cf97bf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/Skin.java @@ -17,12 +17,8 @@ */ package org.jackhuang.hmcl.game.skin; -import javafx.scene.image.Image; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public record Skin(@NotNull TextureModel model, @NotNull TextureObject skin, @Nullable TextureObject cape) { - public record TextureObject(@NotNull Image image, @NotNull String url) { - - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java new file mode 100644 index 00000000000..cdcc1fb161f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java @@ -0,0 +1,25 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game.skin; + +import javafx.scene.image.Image; +import org.jetbrains.annotations.NotNull; + +public record TextureObject(@NotNull Image image, @NotNull String url) { + +} From fa61fc0d068b684f524f6b54909f4518e4bfc2cf Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 22:52:45 +0800 Subject: [PATCH 13/24] =?UTF-8?q?=E7=99=BE=E5=AE=B6=E6=B3=A8=E9=BE=99?= =?UTF-8?q?=E6=85=A7=20=E5=8D=83=E5=86=9B=E8=B5=B7=E9=BE=99=E5=A8=81=20?= =?UTF-8?q?=E7=A0=A5=E6=B7=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/skin/OfflineAccountSkinPage.java | 28 +++++-------------- .../hmcl/ui/account/skin/SkinPageBase.java | 3 -- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 72d3a2e1985..023bb18e70a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -14,7 +14,6 @@ import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.game.skin.TextureObject; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.FileSelector; import org.jackhuang.hmcl.ui.construct.MultiFileItem; @@ -34,12 +33,7 @@ public class OfflineAccountSkinPage extends SkinPageBase { public OfflineAccountSkinPage(OfflineAccount account) { super(account, null); - skinItem.loadChildren(Arrays.asList( - new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), - new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), - new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), - new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE) - )); + skinItem.loadChildren(Arrays.asList(new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE))); modelCombobox.setConverter(FXUtils.stringConverter(model -> i18n("account.skin.model." + model.modelName))); modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM); @@ -77,8 +71,10 @@ public OfflineAccountSkinPage(OfflineAccount account) { StackPane.setAlignment(settingsBox, Pos.CENTER); settingsBox.setAlignment(Pos.CENTER); - FXUtils.observeWeak(this::loadSkinPreview, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), - skinSelector.valueProperty(), capeSelector.valueProperty()); + FXUtils.observeWeak(() -> { + loadSkinPreview(); + this.setSkin(skinProperty.get()).start(); + }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); loadSkinPreview(); } @@ -87,10 +83,8 @@ private void loadSkinPreview() { OfflineSkinConfig config = getConfig(); config.load().whenComplete(Schedulers.javafx(), (loadedSkin, throwable) -> { if (throwable == null && loadedSkin != null) { - TextureObject skinTex = loadedSkin.skin() != null - ? new TextureObject(loadedSkin.skin().image(), "") : null; - TextureObject capeTex = loadedSkin.cape() != null - ? new TextureObject(loadedSkin.cape().image(), "") : null; + TextureObject skinTex = loadedSkin.skin() != null ? new TextureObject(loadedSkin.skin().image(), "") : null; + TextureObject capeTex = loadedSkin.cape() != null ? new TextureObject(loadedSkin.cape().image(), "") : null; if (skinTex != null || capeTex != null) { skinProperty.set(new Skin(loadedSkin.model(), skinTex, capeTex)); @@ -112,12 +106,4 @@ protected ReadOnlyObjectProperty skinObjectProperty() { if (skinProperty == null) skinProperty = new ReadOnlyObjectWrapper<>(); return skinProperty.getReadOnlyProperty(); } - - @Override - protected Task setSkin(Skin skin) { - return Task.supplyAsync(() -> { - account.setSkin(getConfig()); - return null; - }); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 6b590cd4dd9..039769f081c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -28,7 +28,6 @@ import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.skin.Skin; import org.jackhuang.hmcl.game.skin.TextureModel; -import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -95,8 +94,6 @@ public ReadOnlyObjectProperty stateProperty() { protected abstract ReadOnlyObjectProperty skinObjectProperty(); - protected abstract Task setSkin(Skin skin); - protected final class SkinManage extends HBox { protected StackPane leftRegion = new StackPane(); private BorderPane rightRegion = new BorderPane(); From 2ce6805a69be4fdc0ac81ff2d4ab46b5901a6bc6 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 22:53:52 +0800 Subject: [PATCH 14/24] =?UTF-8?q?`=E5=A6=99=E7=AC=94=E7=94=9F=E6=96=87?= =?UTF-8?q?=E7=A9=97=20=E7=BD=A1=E9=A3=8E=E6=8A=9A=E9=95=BF=E9=BA=BE`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 023bb18e70a..267ebc5d525 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -73,7 +73,7 @@ public OfflineAccountSkinPage(OfflineAccount account) { FXUtils.observeWeak(() -> { loadSkinPreview(); - this.setSkin(skinProperty.get()).start(); + account.setSkin(getConfig()); }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); loadSkinPreview(); From c8699539cb88bde1387cb6e525937e921eb05489 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 22:56:18 +0800 Subject: [PATCH 15/24] =?UTF-8?q?=E5=A7=8B=E8=A7=81=E9=BE=99=E5=BD=A2?= =?UTF-8?q?=E6=B1=87=20=E4=BB=A5=E5=A4=A9=E7=94=B0=E5=86=B2=E8=85=BE?= =?UTF-8?q?=E7=9B=B4=E5=90=91=E4=B9=9D=E9=99=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/skin/OfflineAccountSkinPage.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 267ebc5d525..63fbb013304 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ package org.jackhuang.hmcl.ui.account.skin; import com.jfoenix.controls.JFXComboBox; From 8f7dd27dd4bebe4ee1bc3fcd8622b6502da2cf46 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 23:12:01 +0800 Subject: [PATCH 16/24] =?UTF-8?q?=E9=BE=99=E9=9C=87=E4=BA=8E=E7=96=86=20?= =?UTF-8?q?=E4=B8=87=E9=87=8C=E5=AE=81=E5=A3=A4=20=E5=A4=A9=E5=9C=B0?= =?UTF-8?q?=E7=9A=86=E5=8F=AF=E5=BE=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/OfflineAccountSkinPane.java | 202 ------------------ 1 file changed, 202 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java deleted file mode 100644 index 32ba56abaae..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.account; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXComboBox; -import com.jfoenix.controls.JFXDialogLayout; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.input.DragEvent; -import javafx.scene.input.TransferMode; -import javafx.scene.layout.*; -import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; -import org.jackhuang.hmcl.game.TexturesLoader; -import org.jackhuang.hmcl.game.skin.TextureModel; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.FileSelector; -import org.jackhuang.hmcl.ui.construct.MultiFileItem; -import org.jackhuang.hmcl.ui.skin.SkinCanvas; -import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning; -import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms; -import org.jackhuang.hmcl.util.io.FileUtils; - -import java.nio.file.Path; -import java.util.Arrays; -import java.util.UUID; - -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public class OfflineAccountSkinPane extends StackPane { - private final OfflineAccount account; - - private final MultiFileItem skinItem = new MultiFileItem<>(); - private final JFXComboBox modelCombobox = new JFXComboBox<>(); - 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"))); - - BorderPane pane = new BorderPane(); - - SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 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()) { - Path file = e.getDragboard().getFiles().get(0).toPath(); - if (FileUtils.getName(file).endsWith(".png")) - e.acceptTransferModes(TransferMode.COPY); - } - }); - canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> { - if (e.isAccepted()) { - Path skin = e.getDragboard().getFiles().get(0).toPath(); - Platform.runLater(() -> { - skinSelector.setValue(FileUtils.getAbsolutePath(skin)); - skinItem.setSelectedData(OfflineSkinConfig.Type.LOCAL_FILE); - }); - } - }); - - StackPane skinOptionPane = new StackPane(); - skinOptionPane.setMaxWidth(300); - VBox optionPane = new VBox(skinItem, skinOptionPane); - pane.setRight(optionPane); - - skinSelector.maxWidthProperty().bind(skinOptionPane.maxWidthProperty().multiply(0.7)); - capeSelector.maxWidthProperty().bind(skinOptionPane.maxWidthProperty().multiply(0.7)); - - layout.setBody(pane); - - skinItem.loadChildren(Arrays.asList( - new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), - new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), - new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), - new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE) - )); - - modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName))); - modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM); - - if (account.getSkin() == null) { - skinItem.setSelectedData(OfflineSkinConfig.Type.DEFAULT); - modelCombobox.setValue(TextureModel.WIDE); - } else { - skinItem.setSelectedData(account.getSkin().type()); - modelCombobox.setValue(account.getSkin().textureModel()); - skinSelector.setValue(account.getSkin().localSkinPath()); - capeSelector.setValue(account.getSkin().localCapePath()); - } - - skinBinding = FXUtils.observeWeak(() -> { - getSkin().load() - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - LOG.warning("Failed to load skin", exception); - Controllers.showToast(i18n("message.failed")); - } else { - UUID uuid = this.account.getUUID(); - if (result == null || result.skin() == null && result.cape() == null) { - canvas.updateSkin( - TexturesLoader.getDefaultSkin(uuid).image(), - TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM, - null - ); - return; - } - canvas.updateSkin( - result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).image(), - result.model() == TextureModel.SLIM, - result.cape() != null ? result.cape().image() : null); - } - }).start(); - }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); - - FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> { - GridPane gridPane = new GridPane(); - // Increase bottom padding to prevent the prompt from overlapping with the dialog action area - - gridPane.setPadding(new Insets(0, 0, 45, 10)); - gridPane.setHgap(16); - gridPane.setVgap(8); - gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing()); - - switch (selectedData) { - case DEFAULT: - case STEVE: - case ALEX: - break; - case LOCAL_FILE: - gridPane.setPadding(new Insets(0, 0, 0, 10)); - gridPane.addRow(0, new Label(i18n("account.skin.model")), modelCombobox); - gridPane.addRow(1, new Label(i18n("account.skin")), skinSelector); - gridPane.addRow(2, new Label(i18n("account.cape")), capeSelector); - break; - } - - skinOptionPane.getChildren().setAll(gridPane); - }); - - JFXButton acceptButton = new JFXButton(i18n("button.ok")); - acceptButton.getStyleClass().add("dialog-accept"); - acceptButton.setOnAction(e -> { - account.setSkin(getSkin()); - fireEvent(new DialogCloseEvent()); - }); - - JFXButton cancelButton = new JFXButton(i18n("button.cancel")); - cancelButton.getStyleClass().add("dialog-cancel"); - cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - onEscPressed(this, cancelButton::fire); - - layout.setActions(acceptButton, cancelButton); - } - - private OfflineSkinConfig getSkin() { - OfflineSkinConfig.Type type = skinItem.getSelectedData(); - if (type == OfflineSkinConfig.Type.LOCAL_FILE) { - return new OfflineSkinConfig(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); - } else { - return new OfflineSkinConfig(type, null, null, null); - } - } -} From 4c411c473a22306e2f6f9c162ab54d774c31b24b Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 23:17:07 +0800 Subject: [PATCH 17/24] =?UTF-8?q?=E9=BE=99=E7=A7=80=E4=BA=8E=E8=B1=A1=20?= =?UTF-8?q?=E5=BC=95=E4=BB=99=E6=9D=A5=E8=AE=BF=20=E8=AF=97=E8=9C=80?= =?UTF-8?q?=E9=81=93=E6=B2=B3=E6=B1=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 039769f081c..1d227fdc642 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -96,7 +96,7 @@ public ReadOnlyObjectProperty stateProperty() { protected final class SkinManage extends HBox { protected StackPane leftRegion = new StackPane(); - private BorderPane rightRegion = new BorderPane(); + private final BorderPane rightRegion = new BorderPane(); private SkinManage() { setSpacing(10); From 04db7b45cfcd1cbc5ea70fa3224081d69be2ae1f Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Fri, 1 May 2026 23:31:53 +0800 Subject: [PATCH 18/24] =?UTF-8?q?=E9=BE=99=E6=98=8E=E4=BA=8E=E7=AB=A0=20?= =?UTF-8?q?=E6=89=A7=E7=AC=94=E6=88=90=E9=89=B4=20=E6=98=A0=E4=BA=94?= =?UTF-8?q?=E5=8D=83=E7=85=8C=E7=85=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java | 4 +++- HMCL/src/main/java/org/jackhuang/hmcl/ui/skin/SkinCanvas.java | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 1d227fdc642..8895cc6cffa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -113,7 +113,9 @@ private SkinManage() { var skin = TexturesLoader.getDefaultSkin(uuid).image(); var slim = TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM; - SkinCanvas canvas = new SkinCanvas(skin, 250, 300, true); + SkinCanvas canvas = new SkinCanvas(skin, 250, 400, true); + canvas.getScale().setX(1.25); + canvas.getScale().setY(1.25); canvas.updateSkin(skin, slim, null); skinObjectProperty().addListener((obs, oldSkin, newSkin) -> { canvas.updateSkin(newSkin.skin().image(), newSkin.model().isSlim(), newSkin.cape() != null ? newSkin.cape().image() : null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/skin/SkinCanvas.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/skin/SkinCanvas.java index 5bf3cc4c1c8..ddd0c394223 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/skin/SkinCanvas.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/skin/SkinCanvas.java @@ -113,6 +113,10 @@ public Image getSkin() { return skin; } + public Scale getScale() { + return scale; + } + public void updateSkin(Image skin, boolean isSlim, final @Nullable Image cape) { if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) { this.srcSkin = skin; From c678116fd15db7a93d79497efd81135593dde98a Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Sun, 3 May 2026 23:39:25 +0800 Subject: [PATCH 19/24] =?UTF-8?q?=E9=BE=99=E6=B3=BD=E4=BA=8E=E6=B1=A4=20?= =?UTF-8?q?=E5=94=A4=E6=B0=B4=E7=AD=91=E6=B1=9F=20=E5=8D=95=E8=88=9F?= =?UTF-8?q?=E8=A7=81=E4=BA=AC=E6=9D=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/skin/SkinPageBase.java | 48 +++- .../hmcl/ui/construct/IconedItem.java | 12 +- .../org/jackhuang/hmcl/util/SwingFXUtils.java | 222 ++++++++++++++++++ config/checkstyle/checkstyle.xml | 2 +- 4 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 8895cc6cffa..43a4d8c6617 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -24,10 +24,12 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.scene.layout.*; +import javafx.stage.FileChooser; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.skin.Skin; import org.jackhuang.hmcl.game.skin.TextureModel; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -37,9 +39,17 @@ import org.jackhuang.hmcl.ui.skin.SkinCanvas; import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning; import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.SwingFXUtils; import org.jetbrains.annotations.Nullable; +import javax.imageio.ImageIO; +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public abstract class SkinPageBase extends DecoratorAnimatedPage implements DecoratorPage, PageAware { protected final T account; @@ -72,10 +82,32 @@ protected SkinPageBase(T account, @Nullable String url) { PopupMenu saveList = new PopupMenu(); JFXPopup savePopup = new JFXPopup(saveList); - saveList.getContent().setAll(new IconedMenuItem(SVG.APPAREL, i18n("account.skin.manage.save.skin"), () -> { - }, savePopup), new IconedMenuItem(SVG.CROP_9_16, i18n("account.skin.manage.save.cape"), () -> { - }, savePopup)); + var capeItem = new IconedMenuItem(SVG.CROP_9_16, i18n("account.skin.manage.save.cape"), () -> { + var fxCapeImage = skinObjectProperty().get().cape().image(); + var bufferedCapeImage = SwingFXUtils.fromFXImage(fxCapeImage, null); + try { + savePng(bufferedCapeImage); + } catch (Exception e) { + LOG.warning("Failed to export skin img", e); + Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(e), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } + }, savePopup); + + saveList.getContent().setAll(new IconedMenuItem(SVG.APPAREL, i18n("account.skin.manage.save.skin"), () -> { + var fxSkinImage = skinObjectProperty().get().skin().image(); + var bufferedSkinImage = SwingFXUtils.fromFXImage(fxSkinImage, null); + try { + savePng(bufferedSkinImage); + } catch (Exception e) { + LOG.warning("Failed to export skin img", e); + Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(e), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } + }, savePopup), capeItem); + + skinObjectProperty().addListener((observable, oldValue, newValue) -> { + capeItem.setDisable(newValue.cape() == null); + }); AdvancedListBox toolbar = new AdvancedListBox().addNavigationDrawerItem(i18n("button.save"), SVG.OUTPUT, null, item -> { item.setOnAction(e -> savePopup.show(item, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, item.getWidth(), 0)); }); @@ -87,6 +119,16 @@ protected SkinPageBase(T account, @Nullable String url) { this.state.set(State.fromTitle(i18n("account.skin.manage", account.getUsername()))); } + public void savePng(RenderedImage image) throws IOException { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("button.save_as")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*.png")); + fileChooser.setInitialFileName("skin.png"); + File target = fileChooser.showSaveDialog(Controllers.getStage()); + if (target == null) return; + ImageIO.write(image, "png", target); + } + @Override public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java index e45aa02af15..cb05752118d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java @@ -23,7 +23,7 @@ public class IconedItem extends RipplerContainer { - private Label label; + private final Label label; public IconedItem(Node icon, String text) { this(icon); @@ -34,6 +34,16 @@ public IconedItem(Node icon) { super(createHBox(icon)); label = ((Label) lookup("#label")); getStyleClass().setAll("iconed-item"); + + this.disabledProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + this.setOpacity(0.4); + this.setMouseTransparent(true); // 确保不可点击 + } else { + this.setOpacity(1.0); + this.setMouseTransparent(false); + } + }); } private static HBox createHBox(Node icon) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java new file mode 100644 index 00000000000..850863214f1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -0,0 +1,222 @@ +// Copy from javafx.swing +/* + * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.jackhuang.hmcl.util; + +import javafx.scene.image.Image; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelReader; +import javafx.scene.image.WritablePixelFormat; +import javafx.scene.paint.Color; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.nio.IntBuffer; + +/** + * This class provides utility methods for converting data types between + * Swing/AWT and JavaFX formats. + * @since JavaFX 2.2 + */ +public class SwingFXUtils { + private SwingFXUtils() {} // no instances + + /** + * Determine the optimal BufferedImage type to use for the specified + * {@code fxFormat} allowing for the specified {@code bimg} to be used + * as a potential default storage space if it is not null and is compatible. + * + * @param fxFormat the PixelFormat of the source FX Image + * @param bimg an optional existing {@code BufferedImage} to be used + * for storage if it is compatible, or null + * @return + */ + static int + getBestBufferedImageType(PixelFormat fxFormat, BufferedImage bimg, + boolean isOpaque) + { + if (bimg != null) { + int bimgType = bimg.getType(); + if (bimgType == BufferedImage.TYPE_INT_ARGB || + bimgType == BufferedImage.TYPE_INT_ARGB_PRE || + (isOpaque && + (bimgType == BufferedImage.TYPE_INT_BGR || + bimgType == BufferedImage.TYPE_INT_RGB))) + { + // We will allow the caller to give us a BufferedImage + // that has an alpha channel, but we might not otherwise + // construct one ourselves. + // We will also allow them to choose their own premultiply + // type which may not match the image. + // If left to our own devices we might choose a more specific + // format as indicated by the choices below. + return bimgType; + } + } + switch (fxFormat.getType()) { + default: + case BYTE_BGRA_PRE: + case INT_ARGB_PRE: + return BufferedImage.TYPE_INT_ARGB_PRE; + case BYTE_BGRA: + case INT_ARGB: + return BufferedImage.TYPE_INT_ARGB; + case BYTE_RGB: + return BufferedImage.TYPE_INT_RGB; + case BYTE_INDEXED: + return (fxFormat.isPremultiplied() + ? BufferedImage.TYPE_INT_ARGB_PRE + : BufferedImage.TYPE_INT_ARGB); + } + } + + /** + * Determine the appropriate {@link WritablePixelFormat} type that can + * be used to transfer data into the indicated BufferedImage. + * + * @param bimg the BufferedImage that will be used as a destination for + * a {@code PixelReader#getPixels()} operation. + * @return + */ + private static WritablePixelFormat + getAssociatedPixelFormat(BufferedImage bimg) + { + switch (bimg.getType()) { + // We lie here for xRGB, but we vetted that the src data was opaque + // so we can ignore the alpha. We use ArgbPre instead of Argb + // just to get a loop that does not have divides in it if the + // PixelReader happens to not know the data is opaque. + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + return PixelFormat.getIntArgbPreInstance(); + case BufferedImage.TYPE_INT_ARGB: + return PixelFormat.getIntArgbInstance(); + default: + // Should not happen... + throw new InternalError("Failed to validate BufferedImage type"); + } + } + + private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) { + for (int x = 0; x < iw; x++) { + for (int y = 0; y < ih; y++) { + Color color = pr.getColor(x,y); + if (color.getOpacity() != 1.0) { + return false; + } + } + } + return true; + } + + /** + * Snapshots the specified JavaFX {@link Image} object and stores a + * copy of its pixels into a {@link BufferedImage} object, creating + * a new object if needed. + * The method will only convert a JavaFX {@code Image} that is readable + * as per the conditions on the + * {@link Image#getPixelReader() Image.getPixelReader()} + * method. + * If the {@code Image} is not readable, as determined by its + * {@code getPixelReader()} method, then this method will return null. + * If the {@code Image} is a writable, or other dynamic image, then + * the {@code BufferedImage} will only be set to the current state of + * the pixels in the image as determined by its {@link PixelReader}. + * Further changes to the pixels of the {@code Image} will not be + * reflected in the returned {@code BufferedImage}. + *

+ * The optional {@code BufferedImage} parameter may be reused to store + * the copy of the pixels. + * A new {@code BufferedImage} will be created if the supplied object + * is null, is too small or of a type which the image pixels cannot + * be easily converted into. + * + * @param img the JavaFX {@code Image} to be converted + * @param bimg an optional {@code BufferedImage} object that may be + * used to store the returned pixel data + * @return a {@code BufferedImage} containing a snapshot of the JavaFX + * {@code Image}, or null if the {@code Image} is not readable. + * @since JavaFX 2.2 + */ + public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { + PixelReader pr = img.getPixelReader(); + if (pr == null) { + return null; + } + int iw = (int) img.getWidth(); + int ih = (int) img.getHeight(); + PixelFormat fxFormat = pr.getPixelFormat(); + boolean srcPixelsAreOpaque = false; + switch (fxFormat.getType()) { + case INT_ARGB_PRE: + case INT_ARGB: + case BYTE_BGRA_PRE: + case BYTE_BGRA: + // Check fx image opacity only if + // supplied BufferedImage is without alpha channel + if (bimg != null && + (bimg.getType() == BufferedImage.TYPE_INT_BGR || + bimg.getType() == BufferedImage.TYPE_INT_RGB)) { + srcPixelsAreOpaque = checkFXImageOpaque(pr, iw, ih); + } + break; + case BYTE_RGB: + srcPixelsAreOpaque = true; + break; + } + int prefBimgType = getBestBufferedImageType(pr.getPixelFormat(), bimg, srcPixelsAreOpaque); + if (bimg != null) { + int bw = bimg.getWidth(); + int bh = bimg.getHeight(); + if (bw < iw || bh < ih || bimg.getType() != prefBimgType) { + bimg = null; + } else if (iw < bw || ih < bh) { + Graphics2D g2d = bimg.createGraphics(); + g2d.setComposite(AlphaComposite.Clear); + g2d.fillRect(0, 0, bw, bh); + g2d.dispose(); + } + } + if (bimg == null) { + bimg = new BufferedImage(iw, ih, prefBimgType); + } + DataBufferInt db = (DataBufferInt)bimg.getRaster().getDataBuffer(); + int data[] = db.getData(); + int offset = bimg.getRaster().getDataBuffer().getOffset(); + int scan = 0; + SampleModel sm = bimg.getRaster().getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + scan = ((SinglePixelPackedSampleModel)sm).getScanlineStride(); + } + + WritablePixelFormat pf = getAssociatedPixelFormat(bimg); + pr.getPixels(0, 0, iw, ih, pf, data, offset, scan); + return bimg; + } +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 37553e27c78..e4b0c13571a 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -131,7 +131,7 @@ - + From 56081635a994fe4e49b28e8fc540917767d604d9 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Sun, 3 May 2026 23:43:28 +0800 Subject: [PATCH 20/24] =?UTF-8?q?=E9=BE=99=E5=81=A5=E4=BA=8E=E5=B8=B8=20?= =?UTF-8?q?=E7=99=BE=E9=9F=B3=E5=90=8C=E8=AE=B2=20=E9=81=93=E4=B8=80?= =?UTF-8?q?=E7=A7=8D=E7=82=8E=E9=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/util/SwingFXUtils.java | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java index 850863214f1..82198a765bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -42,10 +42,12 @@ /** * This class provides utility methods for converting data types between * Swing/AWT and JavaFX formats. + * * @since JavaFX 2.2 */ -public class SwingFXUtils { - private SwingFXUtils() {} // no instances +public final class SwingFXUtils { + private SwingFXUtils() { + } // no instances /** * Determine the optimal BufferedImage type to use for the specified @@ -53,22 +55,14 @@ private SwingFXUtils() {} // no instances * as a potential default storage space if it is not null and is compatible. * * @param fxFormat the PixelFormat of the source FX Image - * @param bimg an optional existing {@code BufferedImage} to be used - * for storage if it is compatible, or null + * @param bimg an optional existing {@code BufferedImage} to be used + * for storage if it is compatible, or null * @return */ - static int - getBestBufferedImageType(PixelFormat fxFormat, BufferedImage bimg, - boolean isOpaque) - { + static int getBestBufferedImageType(PixelFormat fxFormat, BufferedImage bimg, boolean isOpaque) { if (bimg != null) { int bimgType = bimg.getType(); - if (bimgType == BufferedImage.TYPE_INT_ARGB || - bimgType == BufferedImage.TYPE_INT_ARGB_PRE || - (isOpaque && - (bimgType == BufferedImage.TYPE_INT_BGR || - bimgType == BufferedImage.TYPE_INT_RGB))) - { + if (bimgType == BufferedImage.TYPE_INT_ARGB || bimgType == BufferedImage.TYPE_INT_ARGB_PRE || (isOpaque && (bimgType == BufferedImage.TYPE_INT_BGR || bimgType == BufferedImage.TYPE_INT_RGB))) { // We will allow the caller to give us a BufferedImage // that has an alpha channel, but we might not otherwise // construct one ourselves. @@ -90,9 +84,7 @@ private SwingFXUtils() {} // no instances case BYTE_RGB: return BufferedImage.TYPE_INT_RGB; case BYTE_INDEXED: - return (fxFormat.isPremultiplied() - ? BufferedImage.TYPE_INT_ARGB_PRE - : BufferedImage.TYPE_INT_ARGB); + return (fxFormat.isPremultiplied() ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB); } } @@ -104,9 +96,7 @@ private SwingFXUtils() {} // no instances * a {@code PixelReader#getPixels()} operation. * @return */ - private static WritablePixelFormat - getAssociatedPixelFormat(BufferedImage bimg) - { + private static WritablePixelFormat getAssociatedPixelFormat(BufferedImage bimg) { switch (bimg.getType()) { // We lie here for xRGB, but we vetted that the src data was opaque // so we can ignore the alpha. We use ArgbPre instead of Argb @@ -126,7 +116,7 @@ private SwingFXUtils() {} // no instances private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) { for (int x = 0; x < iw; x++) { for (int y = 0; y < ih; y++) { - Color color = pr.getColor(x,y); + Color color = pr.getColor(x, y); if (color.getOpacity() != 1.0) { return false; } @@ -157,11 +147,11 @@ private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) { * is null, is too small or of a type which the image pixels cannot * be easily converted into. * - * @param img the JavaFX {@code Image} to be converted + * @param img the JavaFX {@code Image} to be converted * @param bimg an optional {@code BufferedImage} object that may be - * used to store the returned pixel data + * used to store the returned pixel data * @return a {@code BufferedImage} containing a snapshot of the JavaFX - * {@code Image}, or null if the {@code Image} is not readable. + * {@code Image}, or null if the {@code Image} is not readable. * @since JavaFX 2.2 */ public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { @@ -180,9 +170,7 @@ public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { case BYTE_BGRA: // Check fx image opacity only if // supplied BufferedImage is without alpha channel - if (bimg != null && - (bimg.getType() == BufferedImage.TYPE_INT_BGR || - bimg.getType() == BufferedImage.TYPE_INT_RGB)) { + if (bimg != null && (bimg.getType() == BufferedImage.TYPE_INT_BGR || bimg.getType() == BufferedImage.TYPE_INT_RGB)) { srcPixelsAreOpaque = checkFXImageOpaque(pr, iw, ih); } break; @@ -206,13 +194,13 @@ public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { if (bimg == null) { bimg = new BufferedImage(iw, ih, prefBimgType); } - DataBufferInt db = (DataBufferInt)bimg.getRaster().getDataBuffer(); + DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer(); int data[] = db.getData(); int offset = bimg.getRaster().getDataBuffer().getOffset(); - int scan = 0; + int scan = 0; SampleModel sm = bimg.getRaster().getSampleModel(); if (sm instanceof SinglePixelPackedSampleModel) { - scan = ((SinglePixelPackedSampleModel)sm).getScanlineStride(); + scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride(); } WritablePixelFormat pf = getAssociatedPixelFormat(bimg); From 638eb6a169253010b7b52a8774a72c75e212dec3 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Mon, 4 May 2026 00:00:23 +0800 Subject: [PATCH 21/24] =?UTF-8?q?=E9=BE=99=E6=99=AF=E4=BA=8E=E5=BA=B7=20?= =?UTF-8?q?=E8=A7=81=E4=B9=8B=E5=BA=99=E5=A0=82=20=E4=BA=A6=E6=98=BE?= =?UTF-8?q?=E4=BA=8E=E6=9B=B2=E5=9D=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/skin/OfflineAccountSkinPage.java | 99 +++++++++++++++---- .../hmcl/ui/account/skin/SkinPageBase.java | 8 +- .../org/jackhuang/hmcl/util/io/FileUtils.java | 2 +- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 63fbb013304..6d32cca4ca2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -18,8 +18,10 @@ package org.jackhuang.hmcl.ui.account.skin; import com.jfoenix.controls.JFXComboBox; +import javafx.beans.InvalidationListener; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; @@ -27,17 +29,21 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig; +import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.skin.Skin; import org.jackhuang.hmcl.game.skin.TextureModel; import org.jackhuang.hmcl.game.skin.TextureObject; import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.FileSelector; import org.jackhuang.hmcl.ui.construct.MultiFileItem; import java.util.Arrays; +import java.util.UUID; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class OfflineAccountSkinPage extends SkinPageBase { private ReadOnlyObjectWrapper skinProperty; @@ -50,7 +56,12 @@ public class OfflineAccountSkinPage extends SkinPageBase { public OfflineAccountSkinPage(OfflineAccount account) { super(account, null); - skinItem.loadChildren(Arrays.asList(new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE))); + skinItem.loadChildren(Arrays.asList( + new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), + new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), + new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), + new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), OfflineSkinConfig.Type.LOCAL_FILE) + )); modelCombobox.setConverter(FXUtils.stringConverter(model -> i18n("account.skin.model." + model.modelName))); modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM); @@ -74,48 +85,96 @@ public OfflineAccountSkinPage(OfflineAccount account) { grid.setHgap(16); grid.setVgap(10); - skinItem.selectedDataProperty().addListener((obs, oldVal, newVal) -> { + ChangeListener listener = (obs, oldVal, newVal) -> { grid.getChildren().clear(); if (newVal == OfflineSkinConfig.Type.LOCAL_FILE) { grid.addRow(0, new Label(i18n("account.skin.model")), modelCombobox); grid.addRow(1, new Label(i18n("account.skin")), skinSelector); grid.addRow(2, new Label(i18n("account.cape")), capeSelector); } - }); + }; + + listener.changed(null, null, skinItem.getSelectedData()); + skinItem.selectedDataProperty().addListener(listener); settingsBox.getChildren().addAll(skinItem, grid); contentPane.getChildren().setAll(settingsBox); StackPane.setAlignment(settingsBox, Pos.CENTER); settingsBox.setAlignment(Pos.CENTER); - FXUtils.observeWeak(() -> { - loadSkinPreview(); +// super.skinManage.setOnDragOver(e -> { +// if (e.getDragboard().hasFiles()) { +// Path file = e.getDragboard().getFiles().get(0).toPath(); +// if (FileUtils.getName(file).endsWith(".png")) { +// e.acceptTransferModes(TransferMode.COPY); +// } +// } +// }); +// super.skinManage.setOnDragDropped(e -> { +// if (e.isAccepted()) { +// Path skin = e.getDragboard().getFiles().get(0).toPath(); +// Platform.runLater(() -> { +// skinSelector.setValue(FileUtils.getAbsolutePath(skin)); +// skinItem.setSelectedData(OfflineSkinConfig.Type.LOCAL_FILE); +// }); +// } +// }); + + InvalidationListener invalidationListener = (e) -> { account.setSkin(getConfig()); - }, skinItem.selectedDataProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty()); + loadSkinPreview(); + }; + + skinItem.selectedDataProperty().addListener(invalidationListener); + modelCombobox.valueProperty().addListener(invalidationListener); + skinSelector.valueProperty().addListener(invalidationListener); + capeSelector.valueProperty().addListener(invalidationListener); loadSkinPreview(); } + private OfflineSkinConfig getConfig() { + OfflineSkinConfig.Type type = skinItem.getSelectedData(); + if (type == null) type = OfflineSkinConfig.Type.DEFAULT; + TextureModel model = modelCombobox.getValue(); + + var textureModel = switch (type) { + case ALEX -> TextureModel.SLIM; + case STEVE -> TextureModel.WIDE; + case DEFAULT -> TexturesLoader.getDefaultModel(account.getUUID()); + default -> model; + }; + + return new OfflineSkinConfig(type, textureModel, skinSelector.getValue(), capeSelector.getValue()); + } + private void loadSkinPreview() { OfflineSkinConfig config = getConfig(); config.load().whenComplete(Schedulers.javafx(), (loadedSkin, throwable) -> { - if (throwable == null && loadedSkin != null) { - TextureObject skinTex = loadedSkin.skin() != null ? new TextureObject(loadedSkin.skin().image(), "") : null; - TextureObject capeTex = loadedSkin.cape() != null ? new TextureObject(loadedSkin.cape().image(), "") : null; + if (throwable != null) { + LOG.warning("Failed to load skin for preview", throwable); + Controllers.showToast(i18n("message.failed")); + return; + } + + UUID uuid = account.getUUID(); + TextureModel model = TextureModel.WIDE; + TextureObject skinTex = null; + TextureObject capeTex = null; - if (skinTex != null || capeTex != null) { - skinProperty.set(new Skin(loadedSkin.model(), skinTex, capeTex)); - } + if (loadedSkin != null) { + model = loadedSkin.model(); + skinTex = loadedSkin.skin() != null ? new TextureObject(loadedSkin.skin().image(), "") : null; + capeTex = loadedSkin.cape() != null ? new TextureObject(loadedSkin.cape().image(), "") : null; } - }).start(); - } - private OfflineSkinConfig getConfig() { - OfflineSkinConfig.Type type = skinItem.getSelectedData(); - if (type == OfflineSkinConfig.Type.LOCAL_FILE) { - return new OfflineSkinConfig(type, modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue()); - } - return new OfflineSkinConfig(type, null, null, null); + if (skinTex == null) { + skinTex = new TextureObject(TexturesLoader.getDefaultSkin(uuid).image(), ""); + model = TexturesLoader.getDefaultModel(uuid); + } + + skinProperty.set(new Skin(model, skinTex, capeTex)); + }).start(); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 43a4d8c6617..6ae38633a95 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -87,7 +87,7 @@ protected SkinPageBase(T account, @Nullable String url) { var fxCapeImage = skinObjectProperty().get().cape().image(); var bufferedCapeImage = SwingFXUtils.fromFXImage(fxCapeImage, null); try { - savePng(bufferedCapeImage); + savePng(bufferedCapeImage, "cape"); } catch (Exception e) { LOG.warning("Failed to export skin img", e); Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(e), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); @@ -98,7 +98,7 @@ protected SkinPageBase(T account, @Nullable String url) { var fxSkinImage = skinObjectProperty().get().skin().image(); var bufferedSkinImage = SwingFXUtils.fromFXImage(fxSkinImage, null); try { - savePng(bufferedSkinImage); + savePng(bufferedSkinImage, "skin"); } catch (Exception e) { LOG.warning("Failed to export skin img", e); Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(e), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); @@ -119,11 +119,11 @@ protected SkinPageBase(T account, @Nullable String url) { this.state.set(State.fromTitle(i18n("account.skin.manage", account.getUsername()))); } - public void savePng(RenderedImage image) throws IOException { + public void savePng(RenderedImage image, String name) throws IOException { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("button.save_as")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*.png")); - fileChooser.setInitialFileName("skin.png"); + fileChooser.setInitialFileName(name + ".png"); File target = fileChooser.showSaveDialog(Controllers.getStage()); if (target == null) return; ImageIO.write(image, "png", target); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index bce609927db..94e73fbc8df 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -490,7 +490,7 @@ public static List listFilesByExtension(Path file, String extension) { } public static Optional tryGetPath(String first, String... more) { - if (first == null) return Optional.empty(); + if (first == null || first.isEmpty()) return Optional.empty(); try { return Optional.of(Paths.get(first, more)); } catch (InvalidPathException e) { From f930757b451de45c26244a366d9b34ff58adcfc5 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Mon, 4 May 2026 00:05:23 +0800 Subject: [PATCH 22/24] =?UTF-8?q?=E4=B8=8D=E5=8A=B3=E6=AD=A4=E9=97=B4?= =?UTF-8?q?=E7=A5=A5=E4=BA=91=E7=91=9E=E5=85=BD=E9=A2=91=E9=A2=91=E8=AF=B0?= =?UTF-8?q?=E6=98=A5=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/util/SwingFXUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java index 82198a765bd..17f569e5b49 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java @@ -74,10 +74,6 @@ static int getBestBufferedImageType(PixelFormat fxFormat, BufferedImage bimg, } } switch (fxFormat.getType()) { - default: - case BYTE_BGRA_PRE: - case INT_ARGB_PRE: - return BufferedImage.TYPE_INT_ARGB_PRE; case BYTE_BGRA: case INT_ARGB: return BufferedImage.TYPE_INT_ARGB; @@ -85,6 +81,10 @@ static int getBestBufferedImageType(PixelFormat fxFormat, BufferedImage bimg, return BufferedImage.TYPE_INT_RGB; case BYTE_INDEXED: return (fxFormat.isPremultiplied() ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB); + case BYTE_BGRA_PRE: + case INT_ARGB_PRE: + default: + return BufferedImage.TYPE_INT_ARGB_PRE; } } @@ -195,7 +195,7 @@ public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { bimg = new BufferedImage(iw, ih, prefBimgType); } DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer(); - int data[] = db.getData(); + int[] data = db.getData(); int offset = bimg.getRaster().getDataBuffer().getOffset(); int scan = 0; SampleModel sm = bimg.getRaster().getSampleModel(); From 1aa61a9dd9c508dac21c3b48ed3d5e233cdca141 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Sat, 9 May 2026 23:18:04 +0800 Subject: [PATCH 23/24] =?UTF-8?q?=E8=99=BD=E4=B8=87=E8=A8=80=E7=AB=9F?= =?UTF-8?q?=E9=81=93=E4=B8=8D=E5=B0=BD=E6=97=A0=E5=AD=97=E7=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index fb9ff684bce..b5ac669edae 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -167,7 +167,7 @@ account.skin.type.little_skin=LittleSkin account.skin.type.little_skin.hint=You need to create a player with the same player name as your offline account on your skin provider site. Your skin will now be set to the skin assigned to your player on the skin provider site. account.skin.type.local_file=Local Skin File account.skin.type.steve=Steve -account.skin.upload=Upload/Edit Skin +account.skin.upload=Manage Skin account.skin.upload.failed=Failed to upload skin. account.skin.invalid_skin=Invalid skin file. account.username=Username diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 51564235131..d2a8c102245 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -165,7 +165,7 @@ account.skin.type.little_skin=LittleSkin 皮膚站 account.skin.type.little_skin.hint=你需要在皮膚站中新增並使用和該離線帳戶同名角色。此時離線帳戶外觀將為皮膚站上對應角色所設定的外觀。 account.skin.type.local_file=本機外觀圖片檔案 account.skin.type.steve=Steve -account.skin.upload=上傳/編輯外觀 +account.skin.upload=管理外觀 account.skin.upload.failed=外觀上傳失敗 account.skin.invalid_skin=無法識別的外觀檔案 account.username=使用者名稱 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 3aa759e6272..771f2695525 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -167,7 +167,7 @@ account.skin.type.little_skin=LittleSkin 皮肤站 account.skin.type.little_skin.hint=你需要在皮肤站中创建并使用和该离线账户同名的角色。此时离线账户皮肤将显示为皮肤站上对应角色所设置的皮肤。\n你可以点击右上角帮助按钮进行求助。 account.skin.type.local_file=本地皮肤图片文件 account.skin.type.steve=Steve -account.skin.upload=上传/编辑皮肤 +account.skin.upload=管理皮肤 account.skin.upload.failed=皮肤上传失败 account.skin.invalid_skin=无法识别的皮肤文件 account.username=用户名 From 25b3880e204d09d93dba2a11353da158105f2e85 Mon Sep 17 00:00:00 2001 From: CiiLu <109708109+CiiLu@users.noreply.github.com> Date: Sat, 9 May 2026 23:26:08 +0800 Subject: [PATCH 24/24] =?UTF-8?q?=E4=B8=B4=E6=B8=8A=E4=B9=BE=E4=B9=BE?= =?UTF-8?q?=E5=90=9B=E5=AD=90=E5=82=AC=20=E6=88=96=E8=B7=83=20=E6=97=A0?= =?UTF-8?q?=E5=92=8E=E7=9B=B8=E9=9A=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/skin/OfflineAccountSkinPage.java | 44 +++++++------------ .../hmcl/ui/account/skin/SkinPageBase.java | 23 ++++++++++ .../hmcl/game/skin/TextureObject.java | 7 ++- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java index 6d32cca4ca2..6f07dd13bf8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/OfflineAccountSkinPage.java @@ -38,7 +38,9 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.FileSelector; import org.jackhuang.hmcl.ui.construct.MultiFileItem; +import org.jackhuang.hmcl.util.io.FileUtils; +import java.nio.file.Path; import java.util.Arrays; import java.util.UUID; @@ -48,7 +50,7 @@ public class OfflineAccountSkinPage extends SkinPageBase { private ReadOnlyObjectWrapper skinProperty; - private final MultiFileItem skinItem = new MultiFileItem<>(); + private final MultiFileItem skinTypeItem = new MultiFileItem<>(); private final JFXComboBox modelCombobox = new JFXComboBox<>(); private final FileSelector skinSelector = new FileSelector(); private final FileSelector capeSelector = new FileSelector(); @@ -56,7 +58,7 @@ public class OfflineAccountSkinPage extends SkinPageBase { public OfflineAccountSkinPage(OfflineAccount account) { super(account, null); - skinItem.loadChildren(Arrays.asList( + skinTypeItem.loadChildren(Arrays.asList( new MultiFileItem.Option<>(i18n("message.default"), OfflineSkinConfig.Type.DEFAULT), new MultiFileItem.Option<>(i18n("account.skin.type.steve"), OfflineSkinConfig.Type.STEVE), new MultiFileItem.Option<>(i18n("account.skin.type.alex"), OfflineSkinConfig.Type.ALEX), @@ -68,10 +70,10 @@ public OfflineAccountSkinPage(OfflineAccount account) { OfflineSkinConfig config = account.getSkin(); if (config == null) { - skinItem.setSelectedData(OfflineSkinConfig.Type.DEFAULT); + skinTypeItem.setSelectedData(OfflineSkinConfig.Type.DEFAULT); modelCombobox.setValue(TextureModel.WIDE); } else { - skinItem.setSelectedData(config.type()); + skinTypeItem.setSelectedData(config.type()); modelCombobox.setValue(config.textureModel() != null ? config.textureModel() : TextureModel.WIDE); skinSelector.setValue(config.localSkinPath()); capeSelector.setValue(config.localCapePath()); @@ -94,38 +96,20 @@ public OfflineAccountSkinPage(OfflineAccount account) { } }; - listener.changed(null, null, skinItem.getSelectedData()); - skinItem.selectedDataProperty().addListener(listener); + listener.changed(null, null, skinTypeItem.getSelectedData()); + skinTypeItem.selectedDataProperty().addListener(listener); - settingsBox.getChildren().addAll(skinItem, grid); + settingsBox.getChildren().addAll(skinTypeItem, grid); contentPane.getChildren().setAll(settingsBox); StackPane.setAlignment(settingsBox, Pos.CENTER); settingsBox.setAlignment(Pos.CENTER); -// super.skinManage.setOnDragOver(e -> { -// if (e.getDragboard().hasFiles()) { -// Path file = e.getDragboard().getFiles().get(0).toPath(); -// if (FileUtils.getName(file).endsWith(".png")) { -// e.acceptTransferModes(TransferMode.COPY); -// } -// } -// }); -// super.skinManage.setOnDragDropped(e -> { -// if (e.isAccepted()) { -// Path skin = e.getDragboard().getFiles().get(0).toPath(); -// Platform.runLater(() -> { -// skinSelector.setValue(FileUtils.getAbsolutePath(skin)); -// skinItem.setSelectedData(OfflineSkinConfig.Type.LOCAL_FILE); -// }); -// } -// }); - InvalidationListener invalidationListener = (e) -> { account.setSkin(getConfig()); loadSkinPreview(); }; - skinItem.selectedDataProperty().addListener(invalidationListener); + skinTypeItem.selectedDataProperty().addListener(invalidationListener); modelCombobox.valueProperty().addListener(invalidationListener); skinSelector.valueProperty().addListener(invalidationListener); capeSelector.valueProperty().addListener(invalidationListener); @@ -134,7 +118,7 @@ public OfflineAccountSkinPage(OfflineAccount account) { } private OfflineSkinConfig getConfig() { - OfflineSkinConfig.Type type = skinItem.getSelectedData(); + OfflineSkinConfig.Type type = skinTypeItem.getSelectedData(); if (type == null) type = OfflineSkinConfig.Type.DEFAULT; TextureModel model = modelCombobox.getValue(); @@ -177,6 +161,12 @@ private void loadSkinPreview() { }).start(); } + @Override + protected void onDrag(Path skin) { + skinTypeItem.setSelectedData(OfflineSkinConfig.Type.LOCAL_FILE); + skinSelector.setValue(FileUtils.getAbsolutePath(skin)); + } + @Override protected ReadOnlyObjectProperty skinObjectProperty() { if (skinProperty == null) skinProperty = new ReadOnlyObjectWrapper<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java index 6ae38633a95..a992d91f658 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/skin/SkinPageBase.java @@ -18,11 +18,13 @@ package org.jackhuang.hmcl.ui.account.skin; import com.jfoenix.controls.JFXPopup; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; +import javafx.scene.input.TransferMode; import javafx.scene.layout.*; import javafx.stage.FileChooser; import org.jackhuang.hmcl.auth.Account; @@ -41,12 +43,14 @@ import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.SwingFXUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -114,11 +118,30 @@ protected SkinPageBase(T account, @Nullable String url) { BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); + skinManage.setOnDragOver(e -> { + if (e.getDragboard().hasFiles()) { + Path file = e.getDragboard().getFiles().get(0).toPath(); + if (FileUtils.getName(file).endsWith(".png")) { + e.acceptTransferModes(TransferMode.COPY); + } + } + }); + skinManage.setOnDragDropped(e -> { + if (e.isAccepted()) { + Path skin = e.getDragboard().getFiles().get(0).toPath(); + Platform.runLater(() -> { + onDrag(skin); + }); + } + }); + setCenter(transitionPane); this.state.set(State.fromTitle(i18n("account.skin.manage", account.getUsername()))); } + protected abstract void onDrag(Path skin); + public void savePng(RenderedImage image, String name) throws IOException { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("button.save_as")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java index cdcc1fb161f..396de8d522c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/skin/TextureObject.java @@ -18,8 +18,13 @@ package org.jackhuang.hmcl.game.skin; import javafx.scene.image.Image; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; -public record TextureObject(@NotNull Image image, @NotNull String url) { +import java.nio.file.Path; +public record TextureObject(@NotNull Image image, @NotNull String url) { + public TextureObject of(Path path) { + return new TextureObject(new Image(path.toString()), FileUtils.getAbsolutePath(path)); + } }