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/22] =?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 ab0c15dd72..9bd6026649 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 25467e7e15..1cc6cd331b 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 eac62d2b40..e1c4c75ab9 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 70a9976d32..268d36a6dc 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/22] =?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 9bd6026649..dc2e068700 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 1cc6cd331b..c1d13efb80 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 7e93fa9019..70bcd486c3 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 e1c4c75ab9..f34532a5de 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 268d36a6dc..0e20017fad 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/22] =?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 dc2e068700..7f77bad267 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 c1d13efb80..b45ead91c4 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 70bcd486c3..cbf3912e69 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 e06dbdbb9e..634b4f09f8 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 f34532a5de..9623924db9 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 0e20017fad..d181ca0df8 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/22] =?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 7f77bad267..0de26b43e0 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 fa5de7480a..a5e37791db 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 b45ead91c4..0a45a7d815 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 3d40c983fe..348e6ba1ff 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 6be37269ab..99e05bce8d 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 aa06138053..f58b7dd574 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 883d2d0b74..964b3d0b7c 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 cbf3912e69..880e8eb4c1 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 9623924db9..e68c077742 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 d181ca0df8..93756df09e 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 f253eb40b3..ac6dca85e0 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 edbad40a8a..1cacef86e3 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 8542b471b3..7db7fcb628 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 eb9545a90e..0b852272e9 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 db100eeaae..1e0dc5afba 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/22] =?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 e68c077742..85067983f1 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 93756df09e..e510ca3dc9 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 0000000000..0dc97050f3 --- /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/22] =?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 0de26b43e0..6d8f4c2bab 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 0a45a7d815..ff8906df21 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 348e6ba1ff..38753913bc 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 f58b7dd574..878a5ae0d9 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 964b3d0b7c..ec59385fb3 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 be9690f309..55232f71a0 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 0dc97050f3..6e575541df 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 880e8eb4c1..b8cb924fa5 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 85067983f1..4062e32abf 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 e510ca3dc9..1a64f2a9e9 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 7db7fcb628..8f6534ff20 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 ac6dca85e0..f2b15bc33a 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 1cacef86e3..c468449990 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/22] =?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 0000000000..30fbdf77be --- /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 4062e32abf..5111586e8e 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 1a64f2a9e9..3eaf69ff7f 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 f3c09d8775..a7da58c039 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 c468449990..5234c78afe 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 6e575541df..8d1dfe978a 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/22] =?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 13a67943c1..9a3603f50c 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 0000000000..216c885dd1 --- /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 e13e61bd67..40dad8a7f8 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 555f135af8..64004e4bc1 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 b251db3d68..7bbaf5bad2 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 8d1dfe978a..33cb03bb8f 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/22] =?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 9a3603f50c..d704425fd2 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 a5e37791db..0306255930 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 0000000000..0a8f1c91fa --- /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 216c885dd1..9ac286facc 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 40dad8a7f8..4c8f45382c 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 64004e4bc1..69870abe00 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 7bbaf5bad2..df89f1675d 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 33cb03bb8f..ff647668f5 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 0b852272e9..593a946806 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/22] =?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 0a8f1c91fa..ba25e60956 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 9ac286facc..49e82290ea 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/22] =?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 ff8906df21..32ba56abaa 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/22] =?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 ba25e60956..72d3a2e198 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 49e82290ea..6b590cd4dd 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 ff647668f5..0f5e1cf97b 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 0000000000..cdcc1fb161 --- /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/22] =?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 72d3a2e198..023bb18e70 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 6b590cd4dd..039769f081 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/22] =?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 023bb18e70..267ebc5d52 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/22] =?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 267ebc5d52..63fbb01330 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/22] =?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 32ba56abaa..0000000000 --- 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/22] =?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 039769f081..1d227fdc64 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/22] =?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 1d227fdc64..8895cc6cff 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 5bf3cc4c1c..ddd0c39422 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/22] =?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 8895cc6cff..43a4d8c661 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 e45aa02af1..cb05752118 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 0000000000..850863214f --- /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 37553e27c7..e4b0c13571 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/22] =?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 850863214f..82198a765b 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/22] =?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 63fbb01330..6d32cca4ca 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 43a4d8c661..6ae38633a9 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 bce609927d..94e73fbc8d 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/22] =?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 82198a765b..17f569e5b4 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();