From 30b94d80257b0dc6f7bcaa37f36643cf7410c4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:09:55 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E5=A4=A9=E5=9C=B0=E5=92=8C=E8=80=8C?= =?UTF-8?q?=E4=B8=87=E7=89=A9=E7=9A=86=E7=94=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/account/AccountListItem.java | 9 ++++++ .../hmcl/ui/account/AccountListItemSkin.java | 16 ++++++++++ .../hmcl/ui/account/FriendListPage.java | 32 +++++++++++++++++++ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/auth/microsoft/MicrosoftService.java | 3 +- .../hmcl/auth/yggdrasil/YggdrasilService.java | 5 +-- .../hmcl/game/friend/FriendControl.java | 22 +++++++++++++ .../hmcl/game/friend/FriendItem.java | 27 ++++++++++++++++ .../hmcl/game/friend/FriendResponse.java | 30 +++++++++++++++++ 12 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java 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 fb2c3b99858..30f4964b0c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -73,6 +73,7 @@ public enum SVG { GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"), GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"), GRAPH2("M5 22q-1.25 0-2.125-.875T2 19q0-.975.5625-1.75T4 16.175V14q0-1.25.875-2.125T7 11h4V7.825q-.875-.3-1.4375-1.075T9 5q0-1.25.875-2.125T12 2t2.125.875T15 5q0 .975-.5625 1.75T13 7.825V11h4q1.25 0 2.125.875T20 14v2.175q.875.3 1.4375 1.075T22 19q0 1.25-.875 2.125T19 22t-2.125-.875T16 19q0-.975.5625-1.75T18 16.175V14q0-.425-.2875-.7125T17 13H13v3.175q.875.3 1.4375 1.075T15 19q0 1.25-.875 2.125T12 22t-2.125-.875T9 19q0-.975.5625-1.75T11 16.175V13H7q-.425 0-.7125.2875T6 14v2.175q.875.3 1.4375 1.075T8 19q0 1.25-.875 2.125T5 22Zm0-2q.425 0 .7125-.2875T6 19q0-.425-.2875-.7125T5 18q-.425 0-.7125.2875T4 19q0 .425.2875.7125T5 20Zm7 0q.425 0 .7125-.2875T13 19q0-.425-.2875-.7125T12 18q-.425 0-.7125.2875T11 19q0 .425.2875.7125T12 20Zm7 0q.425 0 .7125-.2875T20 19q0-.425-.2875-.7125T19 18q-.425 0-.7125.2875T18 19q0 .425.2875.7125T19 20ZM12 6q.425 0 .7125-.2875T13 5q0-.425-.2875-.7125T12 4q-.425 0-.7125.2875T11 5q0 .425.2875.7125T12 6Z"), + GROUP("M1 20v-2.8q0-.85.438-1.562T2.6 14.55q1.55-.775 3.15-1.162T9 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T17 17.2V20zm18 0v-3q0-1.1-.612-2.113T16.65 13.15q1.275.15 2.4.513t2.1.887q.9.5 1.375 1.112T23 17v3zM6.175 10.825Q5 9.65 5 8t1.175-2.825T9 4t2.825 1.175T13 8t-1.175 2.825T9 12t-2.825-1.175m11.65 0Q16.65 12 15 12q-.275 0-.7-.062t-.7-.138q.675-.8 1.038-1.775T15 8t-.362-2.025T13.6 4.2q.35-.125.7-.163T15 4q1.65 0 2.825 1.175T19 8t-1.175 2.825M3 18h12v-.8q0-.275-.137-.5t-.363-.35q-1.35-.675-2.725-1.012T9 15t-2.775.338T3.5 16.35q-.225.125-.363.35T3 17.2zm7.413-8.587Q11 8.825 11 8t-.587-1.412T9 6t-1.412.588T7 8t.588 1.413T9 10t1.413-.587M9 8"), HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 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"), HELP_FILL("M11.95 18q.525 0 .8875-.3625T13.2 16.75q0-.525-.3625-.8875T11.95 15.5q-.525 0-.8875.3625T10.7 16.75q0 .525.3625.8875T11.95 18Zm-.9-3.85H12.9q0-.825.1875-1.3t1.0625-1.3q.65-.65 1.025-1.2375T15.55 8.9q0-1.4-1.025-2.15T12.1 6q-1.425 0-2.3125.75T8.55 8.55l1.65.65q.125-.45.5625-.975T12.1 7.7q.8 0 1.2.4375t.4.9625q0 .5-.3.9375t-.75.8125q-1.1.975-1.35 1.475t-.25 1.825ZM12 22q-2.075 0-3.9-.7875T4.925 19.075q-1.35-1.35-2.1375-3.175T2 12q0-2.075.7875-3.9T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2q2.075 0 3.9.7875T19.075 4.925q1.35 1.35 2.1375 3.175T22 12q0 2.075-.7875 3.9T19.075 19.075q-1.35 1.35-3.175 2.1375T12 22Z"), HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"), 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 fca58b099e3..88e8a4a5025 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 @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; import org.jackhuang.hmcl.auth.yggdrasil.TextureType; @@ -135,6 +136,14 @@ public ObservableBooleanValue canUploadSkin() { } } + public ObservableBooleanValue canAddFriend() { + if (account instanceof AuthlibInjectorAccount || account instanceof MicrosoftAccount) { + return createBooleanBinding(() -> true); + } else { + return createBooleanBinding(() -> false); + } + } + /** * @return the skin upload task, null if no file is selected */ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index b42282760da..d4bde53cf59 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -156,6 +156,22 @@ public AccountListItemSkin(AccountListItem skinnable) { spinnerUpload.getStyleClass().add("small-spinner-pane"); right.getChildren().add(spinnerUpload); + SpinnerPane friendSpinnerPane = new SpinnerPane(); + JFXButton btnFriend = FXUtils.newToggleButton4(SVG.GROUP); + btnFriend.disableProperty().bind(Bindings.not(skinnable.canAddFriend())); + FXUtils.installFastTooltip(btnFriend, i18n("account.friend")); + friendSpinnerPane.setContent(btnFriend); + btnFriend.setOnAction(actionEvent -> { + if (skinnable.getAccount() instanceof MicrosoftAccount) { + Controllers.navigate(new FriendListPage(skinnable.getAccount(), ((MicrosoftAccount) skinnable.getAccount()).getService())); + } else if (skinnable.getAccount() instanceof AuthlibInjectorAccount) { + // TODO +// Controllers.navigate(new FriendListPage(skinnable.getAccount(), ((AuthlibInjectorAccount) skinnable.getAccount()).getYggdrasilService())); + } + }); + right.getChildren().add(friendSpinnerPane); + + JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY); SpinnerPane spinnerCopyUUID = new SpinnerPane(); spinnerCopyUUID.getStyleClass().add("small-spinner-pane"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java new file mode 100644 index 00000000000..4dc92237996 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -0,0 +1,32 @@ +/* + * 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; + +import javafx.scene.layout.Region; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.game.friend.FriendControl; + +public class FriendListPage extends Region { + private final Account account; + private final FriendControl control; + + public FriendListPage(Account account, FriendControl control) { + this.account = account; + this.control = control; + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 199d231bf9c..eba1582efc7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -79,6 +79,7 @@ account.failed.server_response_malformed=Invalid server response. The authentica account.failed.ssl=An SSL error occurred while connecting to the server. Please try updating your Java. account.failed.dns=An SSL error occurred while connecting to the server. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service. account.failed.wrong_account=You have logged in to the wrong account. +account.friend=Friend List account.storage.read_only=The account file was saved by a newer version of HMCL. The current version of HMCL cannot modify accounts. account.hmcl.hint=You need to click "Log in" and complete the process in the opened browser window. account.injector.add=New Auth Server diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 9e571a5b40c..f214cf1ba5b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -82,6 +82,7 @@ account.failed.server_response_malformed=無法解析認證伺服器回應,可 account.failed.ssl=連線伺服器時發生了 SSL 錯誤。可能網站證書已過期或你使用的 Java 版本過低。請嘗試更新 Java。 account.failed.dns=連線伺服器時發生了 SSL 錯誤。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。 account.failed.wrong_account=登入了錯誤的帳戶 +account.friend=好友列表 account.storage.read_only=帳戶檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改帳戶。 account.hmcl.hint=你需要點擊「登入」按鈕,並在開啟的網頁中完成登入 account.injector.add=新增認證伺服器 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 3f339510c78..282ad295792 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -83,6 +83,7 @@ account.failed.server_response_malformed=无法解析认证服务器响应。可 account.failed.ssl=连接服务器时发生了 SSL 错误。可能网站证书已过期或你使用的 Java 版本过低。请尝试更新 Java,或关闭网络代理后再试。\n你可以点击右上角帮助按钮进行求助。 account.failed.dns=连接服务器时发生了 SSL 错误。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 account.failed.wrong_account=登录了错误的账户 +account.friend=好友列表 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 account.hmcl.hint=你需要点击“登录”按钮,并在弹出的网页中完成登录。 account.injector.add=添加认证服务器 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 b65e2587950..e29e0c1bf09 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 @@ -31,6 +31,7 @@ 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.friend.FriendControl; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; @@ -53,7 +54,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class MicrosoftService { +public class MicrosoftService implements FriendControl { private static final String SCOPE = "XboxLive.signin offline_access"; private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS); 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 540ba623d70..b15ba473805 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 @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; import org.jackhuang.hmcl.util.io.FileUtils; @@ -45,10 +46,10 @@ 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 { +public class YggdrasilService implements FriendControl { private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java new file mode 100644 index 00000000000..2c128a17e6b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java @@ -0,0 +1,22 @@ +/* + * 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.friend; + +public interface FriendControl { + FriendResponse getFriendList(); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java new file mode 100644 index 00000000000..7cbcce338a3 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java @@ -0,0 +1,27 @@ +/* + * 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.friend; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.util.gson.JsonSerializable; + +@JsonSerializable +public record FriendItem( + @SerializedName("profileId") String profileId, + @SerializedName("name") String name) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java new file mode 100644 index 00000000000..3122ad1ec6d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java @@ -0,0 +1,30 @@ +/* + * 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.friend; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.util.gson.JsonSerializable; + +import java.util.List; + +@JsonSerializable +public record FriendResponse + (@SerializedName("friends") List friends, + @SerializedName("incomingRequests") List incomingRequests, + @SerializedName("outgoingRequests") List outgoingRequests) { +} From 9baf548c5568083dd9ad33c83170e27fb389f8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:20:13 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E6=88=91=E7=94=9F=E5=88=86=E4=B9=9D?= =?UTF-8?q?=E9=87=8E=EF=BC=8C=E4=BB=A5=E6=AD=A3=E5=9B=9B=E6=96=B9=E8=B1=A1?= =?UTF-8?q?=E8=A7=82=EF=BC=8C=E5=AF=9F=E5=AE=87=E5=AE=99=E6=99=A8=E6=98=8F?= =?UTF-8?q?=E5=8E=BB=E5=A4=8D=E5=A7=8B=E5=85=83=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/AccountListItem.java | 3 +- .../hmcl/ui/account/AccountListItemSkin.java | 7 +-- .../AuthlibInjectorAccountFactory.java | 8 +-- .../AuthlibInjectorProvider.java | 23 +++----- .../hmcl/auth/microsoft/MicrosoftAccount.java | 10 +++- .../hmcl/auth/microsoft/MicrosoftService.java | 17 +++++- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 54 +++++++++++-------- .../auth/yggdrasil/YggdrasilProvider.java | 25 ++++----- .../hmcl/auth/yggdrasil/YggdrasilService.java | 29 +++++++--- .../hmcl/auth/yggdrasil/YggdrasilSession.java | 37 +------------ .../hmcl/game/friend/FriendControl.java | 4 +- 11 files changed, 111 insertions(+), 106 deletions(-) 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 88e8a4a5025..d9f62eb772c 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 @@ -37,6 +37,7 @@ 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.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -137,7 +138,7 @@ public ObservableBooleanValue canUploadSkin() { } public ObservableBooleanValue canAddFriend() { - if (account instanceof AuthlibInjectorAccount || account instanceof MicrosoftAccount) { + if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) { return createBooleanBinding(() -> true); } else { return createBooleanBinding(() -> false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index d4bde53cf59..4d3fc4a42f5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -34,6 +34,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; @@ -162,9 +163,9 @@ public AccountListItemSkin(AccountListItem skinnable) { FXUtils.installFastTooltip(btnFriend, i18n("account.friend")); friendSpinnerPane.setContent(btnFriend); btnFriend.setOnAction(actionEvent -> { - if (skinnable.getAccount() instanceof MicrosoftAccount) { - Controllers.navigate(new FriendListPage(skinnable.getAccount(), ((MicrosoftAccount) skinnable.getAccount()).getService())); - } else if (skinnable.getAccount() instanceof AuthlibInjectorAccount) { + if (skinnable.getAccount() instanceof MicrosoftAccount account) { + Controllers.navigate(new FriendListPage(account, account)); + } else if (skinnable.getAccount() instanceof YggdrasilAccount account) { // TODO // Controllers.navigate(new FriendListPage(skinnable.getAccount(), ((AuthlibInjectorAccount) skinnable.getAccount()).getYggdrasilService())); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java index 59d78879901..22fb68f0363 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java @@ -18,11 +18,7 @@ package org.jackhuang.hmcl.auth.authlibinjector; import com.google.gson.JsonObject; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AccountID; -import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.CharacterSelector; +import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; @@ -92,7 +88,7 @@ static AuthlibInjectorAccount fromStorage( Map properties = JsonUtils.GSON.fromJson( profilePropertiesObject, JsonUtils.mapTypeOf(String.class, String.class)); - GameProfile selected = session.getSelectedProfile(); + GameProfile selected = session.selectedProfile(); ObservableOptionalCache profileRepository = server.getYggdrasilService().getProfileRepository(); profileRepository.put(selected.getId(), new CompleteGameProfile(selected, properties)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java index ee9c05acd89..1bbb3fb800b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java @@ -18,37 +18,30 @@ package org.jackhuang.hmcl.auth.authlibinjector; import org.glavo.uuid.UUIDs; -import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider; import java.net.URI; import java.util.UUID; -public class AuthlibInjectorProvider implements YggdrasilProvider { - - private final String apiRoot; - - public AuthlibInjectorProvider(String apiRoot) { - this.apiRoot = apiRoot; - } +public record AuthlibInjectorProvider(String apiRoot) implements YggdrasilProvider { @Override - public URI getAuthenticationURL() throws AuthenticationException { + public URI getAuthenticationURL() { return URI.create(apiRoot + "authserver/authenticate"); } @Override - public URI getRefreshmentURL() throws AuthenticationException { + public URI getRefreshmentURL() { return URI.create(apiRoot + "authserver/refresh"); } @Override - public URI getValidationURL() throws AuthenticationException { + public URI getValidationURL() { return URI.create(apiRoot + "authserver/validate"); } @Override - public URI getInvalidationURL() throws AuthenticationException { + public URI getInvalidationURL() { return URI.create(apiRoot + "authserver/invalidate"); } @@ -58,12 +51,12 @@ public URI getSkinUploadURL(UUID uuid) throws UnsupportedOperationException { } @Override - public URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException { + public URI getProfilePropertiesURL(UUID uuid) { return URI.create(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDs.toCompactString(uuid)); } @Override - public String toString() { - return apiRoot; + public URI getFriendsURL() { + return URI.create(apiRoot + "api/yggdrasil/minecraftservices/friends"); } } 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 1d7a1de730b..64a32e254cb 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 @@ -23,8 +23,11 @@ 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.friend.FriendControl; +import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import java.io.IOException; import java.nio.file.Path; import java.util.Map; import java.util.Objects; @@ -34,7 +37,7 @@ import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class MicrosoftAccount extends OAuthAccount { +public final class MicrosoftAccount extends OAuthAccount implements FriendControl { protected final MicrosoftService service; protected UUID profileID; @@ -171,6 +174,11 @@ public void clearCache() { service.getProfileRepository().invalidate(profileID); } + @Override + public FriendResponse getFriendList() throws IOException { + return service.getFriendList(session.accessToken()); + } + @Override public String toString() { return "MicrosoftAccount[accountID=" + getAccountID() + ", profileID=" + profileID 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 e29e0c1bf09..ee167e89c28 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 @@ -31,7 +31,7 @@ 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.friend.FriendControl; +import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; @@ -54,7 +54,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class MicrosoftService implements FriendControl { +public class MicrosoftService { private static final String SCOPE = "XboxLive.signin offline_access"; private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS); @@ -313,6 +313,19 @@ private static String request(String url, Object payload) throws AuthenticationE } } + public FriendResponse getFriendList(String accessToken) throws IOException { + HttpURLConnection request = HttpRequest.GET("https://api.minecraftservices.com/friends") + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").createConnection(); + + if (request.getResponseCode() != 200) { + throw new ResponseCodeException("https://api.minecraftservices.com/friends", request.getResponseCode()); + } + + return JsonUtils.fromNonNullJson(NetworkUtils.readFullyAsString(request), FriendResponse.class); + } + public static class XboxAuthorizationException extends AuthenticationException { private final long errorCode; private final String redirect; 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 d178b6951f0..ec4004c2392 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 @@ -21,16 +21,21 @@ import javafx.beans.binding.ObjectBinding; import org.glavo.uuid.UUIDs; import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.game.friend.FriendControl; +import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import java.io.IOException; import java.nio.file.Path; -import java.util.*; +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; -public abstract class YggdrasilAccount extends ClassicAccount { +public abstract class YggdrasilAccount extends ClassicAccount implements FriendControl { protected final YggdrasilService service; protected final UUID profileID; @@ -43,7 +48,7 @@ protected YggdrasilAccount(AccountID accountID, YggdrasilService service, String super(accountID); this.service = requireNonNull(service); this.loginName = requireNonNull(loginName); - this.profileID = requireNonNull(session.getSelectedProfile().getId()); + this.profileID = requireNonNull(session.selectedProfile().getId()); this.session = requireNonNull(session); addProfilePropertiesListener(); @@ -55,23 +60,23 @@ protected YggdrasilAccount(YggdrasilService service, String loginName, String pa this.loginName = requireNonNull(loginName); YggdrasilSession acquiredSession = service.authenticate(loginName, password, randomClientToken()); - if (acquiredSession.getSelectedProfile() == null) { - if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) { + if (acquiredSession.selectedProfile() == null) { + if (acquiredSession.availableProfiles() == null || acquiredSession.availableProfiles().isEmpty()) { throw new NoCharacterException(); } - GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles()); + GameProfile characterToSelect = selector.select(service, acquiredSession.availableProfiles()); session = service.refresh( - acquiredSession.getAccessToken(), - acquiredSession.getClientToken(), + acquiredSession.accessToken(), + acquiredSession.clientToken(), characterToSelect); // response validity has been checked in refresh() } else { session = acquiredSession; } - profileID = session.getSelectedProfile().getId(); + profileID = session.selectedProfile().getId(); authenticated = true; addProfilePropertiesListener(); @@ -93,23 +98,23 @@ public String getLoginName() { @Override public String getProfileName() { - return session.getSelectedProfile().getName(); + return session.selectedProfile().getName(); } @Override public UUID getProfileID() { - return session.getSelectedProfile().getId(); + return session.selectedProfile().getId(); } @Override public synchronized AuthInfo logIn() throws AuthenticationException { if (!authenticated || !session.hasProfileName()) { - if (session.hasProfileName() && service.validate(session.getAccessToken(), session.getClientToken())) { + if (session.hasProfileName() && service.validate(session.accessToken(), session.clientToken())) { authenticated = true; } else { YggdrasilSession acquiredSession; try { - acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null); + acquiredSession = service.refresh(session.accessToken(), session.clientToken(), null); } catch (RemoteAuthenticationException e) { if ("ForbiddenOperationException".equals(e.getRemoteName())) { throw new CredentialExpiredException(e); @@ -117,8 +122,8 @@ public synchronized AuthInfo logIn() throws AuthenticationException { throw e; } } - if (acquiredSession.getSelectedProfile() == null || - !acquiredSession.getSelectedProfile().getId().equals(profileID)) { + if (acquiredSession.selectedProfile() == null || + !acquiredSession.selectedProfile().getId().equals(profileID)) { throw new ServerResponseMalformedException("Selected profile changed"); } if (!acquiredSession.hasProfileName()) { @@ -139,23 +144,23 @@ public synchronized AuthInfo logIn() throws AuthenticationException { public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException { YggdrasilSession acquiredSession = service.authenticate(loginName, password, randomClientToken()); - if (acquiredSession.getSelectedProfile() == null) { - if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) { + if (acquiredSession.selectedProfile() == null) { + if (acquiredSession.availableProfiles() == null || acquiredSession.availableProfiles().isEmpty()) { throw new CharacterDeletedException(); } - GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream() + GameProfile characterToSelect = acquiredSession.availableProfiles().stream() .filter(charatcer -> charatcer.getId().equals(profileID)) .findFirst() .orElseThrow(CharacterDeletedException::new); session = service.refresh( - acquiredSession.getAccessToken(), - acquiredSession.getClientToken(), + acquiredSession.accessToken(), + acquiredSession.clientToken(), characterToSelect); } else { - if (!acquiredSession.getSelectedProfile().getId().equals(profileID)) { + if (!acquiredSession.selectedProfile().getId().equals(profileID)) { throw new CharacterDeletedException(); } session = acquiredSession; @@ -221,13 +226,18 @@ public boolean canUploadSkin() { @Override public void uploadSkin(boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException { - service.uploadSkin(profileID, session.getAccessToken(), isSlim, file); + service.uploadSkin(profileID, session.accessToken(), isSlim, file); } private static String randomClientToken() { return UUIDs.toCompactString(UUID.randomUUID()); } + @Override + public FriendResponse getFriendList() throws IOException { + return service.getFriendList(session.accessToken()); + } + @Override public String toString() { return "YggdrasilAccount[accountID=" + getAccountID() + ", profileID=" + profileID diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java index db90e38f1af..51a18b5db63 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java @@ -27,32 +27,33 @@ */ public interface YggdrasilProvider { - URI getAuthenticationURL() throws AuthenticationException; + URI getAuthenticationURL(); - URI getRefreshmentURL() throws AuthenticationException; + URI getRefreshmentURL(); - URI getValidationURL() throws AuthenticationException; + URI getValidationURL(); - URI getInvalidationURL() throws AuthenticationException; + URI getInvalidationURL(); /** * URL to upload skin. - * + *

* Headers: - * Authentication: Bearer <access token> - * + * Authentication: Bearer <access token> + *

* Payload: - * The payload for this API consists of multipart form data. There are two parts (order does not matter b/c of boundary): - * model: Empty string for the default model and "slim" for the slim model - * file: Raw image file data + * The payload for this API consists of multipart form data. There are two parts (order does not matter b/c of boundary): + * model: Empty string for the default model and "slim" for the slim model + * file: Raw image file data * - * @see https://wiki.vg/Mojang_API#Upload_Skin * @return url to upload skin - * @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed. + * @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed. * @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading. + * @see https://wiki.vg/Mojang_API#Upload_Skin */ URI getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException; URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException; + URI getFriendsURL(); } 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 b15ba473805..1afb1eeaae0 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 @@ -24,12 +24,11 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; -import org.jackhuang.hmcl.game.friend.FriendControl; +import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.HttpMultipartRequest; -import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; import java.io.IOException; @@ -49,7 +48,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class YggdrasilService implements FriendControl { +public class YggdrasilService { private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS); @@ -112,8 +111,8 @@ public YggdrasilSession refresh(String accessToken, String clientToken, GameProf YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken); if (characterToSelect != null) { - if (response.getSelectedProfile() == null || - !response.getSelectedProfile().getId().equals(characterToSelect.getId())) { + if (response.selectedProfile() == null || + !response.selectedProfile().getId().equals(characterToSelect.getId())) { throw new ServerResponseMalformedException("Failed to select character"); } } @@ -169,7 +168,7 @@ public void uploadSkin(UUID uuid, String accessToken, boolean isSlim, Path file) /** * Get complete game profile. - * + *

* Game profile provided from authentication is not complete (no skin data in properties). * * @param uuid the uuid that the character corresponding to. @@ -228,6 +227,20 @@ private static void handleErrorMessage(ErrorResponse response) throws Authentica } } + public FriendResponse getFriendList(String accessToken) throws IOException { + var url = provider.getFriendsURL().toString(); + HttpURLConnection request = HttpRequest.GET(url) + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").createConnection(); + + if (request.getResponseCode() != 200) { + throw new ResponseCodeException(url, request.getResponseCode()); + } + + return JsonUtils.fromNonNullJson(NetworkUtils.readFullyAsString(request), FriendResponse.class); + } + private static String request(URI uri, Object payload) throws AuthenticationException { try { if (payload == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java index 68d46caff96..c32251c96cb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java @@ -31,15 +31,8 @@ import java.util.stream.Collectors; @Immutable -public class YggdrasilSession { - - private final String clientToken; - private final String accessToken; - private final GameProfile selectedProfile; - private final List availableProfiles; - - @Nullable - private final Map userProperties; +public record YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, + List availableProfiles, @Nullable Map userProperties) { public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List availableProfiles, Map userProperties) { this.clientToken = clientToken; @@ -51,32 +44,6 @@ public YggdrasilSession(String clientToken, String accessToken, GameProfile sele if (accessToken != null) Logger.registerAccessToken(accessToken); } - public String getClientToken() { - return clientToken; - } - - public String getAccessToken() { - return accessToken; - } - - /** - * @return nullable (null if no character is selected) - */ - public GameProfile getSelectedProfile() { - return selectedProfile; - } - - /** - * @return nullable (null if the YggdrasilSession is loaded from storage) - */ - public List getAvailableProfiles() { - return availableProfiles; - } - - public Map getUserProperties() { - return userProperties; - } - public boolean hasProfileName() { return selectedProfile != null && StringUtils.isNotBlank(selectedProfile.getName()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java index 2c128a17e6b..af1062113a2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendControl.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.game.friend; +import java.io.IOException; + public interface FriendControl { - FriendResponse getFriendList(); + FriendResponse getFriendList() throws IOException; } From 67dbdb838e147230a0fe4cfe3c7427baf39297d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:29:03 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E7=85=8C=E7=85=8C=E7=9E=BB=E6=B2=B3?= =?UTF-8?q?=E6=B1=89=EF=BC=8C=E6=9A=82=E6=9A=82=E6=AC=B2=E6=B5=AE=E5=A4=A9?= =?UTF-8?q?=E5=8D=97=EF=BC=8C=E4=B8=8D=E6=9B=BE=E8=B4=A5=E6=98=8E=E6=9C=88?= =?UTF-8?q?=E5=87=BA=E4=BA=91=E7=AB=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/AccountListItem.java | 2 +- .../hmcl/ui/account/AccountListItemSkin.java | 45 ++++++++++++++----- .../assets/lang/I18N_zh_CN.properties | 1 + .../AuthlibInjectorProvider.java | 2 +- 4 files changed, 38 insertions(+), 12 deletions(-) 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 d9f62eb772c..5838296d918 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 @@ -139,7 +139,7 @@ public ObservableBooleanValue canUploadSkin() { public ObservableBooleanValue canAddFriend() { if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) { - return createBooleanBinding(() -> true); + return createBooleanBinding(() -> true); } else { return createBooleanBinding(() -> false); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index 4d3fc4a42f5..79e66348a00 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -20,6 +20,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXRadioButton; import com.jfoenix.effects.JFXDepthManager; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.geometry.Pos; import javafx.scene.Cursor; @@ -43,8 +44,11 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import java.io.IOException; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class AccountListItemSkin extends SkinBase { @@ -157,21 +161,42 @@ public AccountListItemSkin(AccountListItem skinnable) { spinnerUpload.getStyleClass().add("small-spinner-pane"); right.getChildren().add(spinnerUpload); - SpinnerPane friendSpinnerPane = new SpinnerPane(); + SpinnerPane spinnerFriend = new SpinnerPane(); JFXButton btnFriend = FXUtils.newToggleButton4(SVG.GROUP); btnFriend.disableProperty().bind(Bindings.not(skinnable.canAddFriend())); FXUtils.installFastTooltip(btnFriend, i18n("account.friend")); - friendSpinnerPane.setContent(btnFriend); + spinnerFriend.setContent(btnFriend); btnFriend.setOnAction(actionEvent -> { - if (skinnable.getAccount() instanceof MicrosoftAccount account) { - Controllers.navigate(new FriendListPage(account, account)); - } else if (skinnable.getAccount() instanceof YggdrasilAccount account) { - // TODO -// Controllers.navigate(new FriendListPage(skinnable.getAccount(), ((AuthlibInjectorAccount) skinnable.getAccount()).getYggdrasilService())); - } - }); - right.getChildren().add(friendSpinnerPane); + spinnerFriend.showSpinner(); + + var account = skinnable.getAccount(); + Task.runAsync(skinnable::refreshAsync) + .whenComplete(exception -> { + if (exception != null) { + Controllers.showToast(Accounts.localizeErrorMessage(exception)); + return; + } + + if (account instanceof MicrosoftAccount microsoftAccount) { + Platform.runLater(() -> Controllers.navigate(new FriendListPage(account, microsoftAccount))); + } else if (account instanceof YggdrasilAccount yggdrasilAccount) { + try { + yggdrasilAccount.getFriendList(); + Platform.runLater(() -> Controllers.navigate(new FriendListPage(account, yggdrasilAccount))); + } catch (IOException e) { + Platform.runLater(() -> { + if (e instanceof ResponseCodeException responseCodeException && responseCodeException.getResponseCode() == 404) { + Controllers.dialog(i18n("account.friend.unsupported")); + } + + Controllers.showToast(Accounts.localizeErrorMessage(e)); + }); + } + } + }).thenRunAsync(Schedulers.javafx(), spinnerFriend::hideSpinner).start(); + }); + right.getChildren().add(spinnerFriend); JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY); SpinnerPane spinnerCopyUUID = new SpinnerPane(); 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 282ad295792..dda35c1d745 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -84,6 +84,7 @@ account.failed.ssl=连接服务器时发生了 SSL 错误。可能网站证书 account.failed.dns=连接服务器时发生了 SSL 错误。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 account.failed.wrong_account=登录了错误的账户 account.friend=好友列表 +account.friend.unsupported=你的外置登录提供商不支持好友列表功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 account.hmcl.hint=你需要点击“登录”按钮,并在弹出的网页中完成登录。 account.injector.add=添加认证服务器 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java index 1bbb3fb800b..5fa4fd4d51e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java @@ -57,6 +57,6 @@ public URI getProfilePropertiesURL(UUID uuid) { @Override public URI getFriendsURL() { - return URI.create(apiRoot + "api/yggdrasil/minecraftservices/friends"); + return URI.create(apiRoot + "minecraftservices/friends"); } } From c4804e680ddb1337b2967b2822c3a67f38029a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:30:39 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E8=B6=8B=E5=90=89=E9=81=BF=E5=87=B6?= =?UTF-8?q?=E9=A1=BD=EF=BC=8C=E6=B6=88=E7=81=BE=E8=A7=A3=E5=8E=84=E4=B9=8B?= =?UTF-8?q?=E9=9A=BE=EF=BC=8C=E6=8E=88=E6=88=91=E4=BB=A5=E5=A4=A9=E4=BA=BA?= =?UTF-8?q?=E5=B1=85=E4=BD=8D=E4=B8=8A=E5=96=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index eba1582efc7..9d90d051f51 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -80,6 +80,7 @@ account.failed.ssl=An SSL error occurred while connecting to the server. Please account.failed.dns=An SSL error occurred while connecting to the server. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service. account.failed.wrong_account=You have logged in to the wrong account. account.friend=Friend List +account.friend.unsupported=Your authentication server does not support friend feature. account.storage.read_only=The account file was saved by a newer version of HMCL. The current version of HMCL cannot modify accounts. account.hmcl.hint=You need to click "Log in" and complete the process in the opened browser window. account.injector.add=New Auth Server diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index f214cf1ba5b..1685a45baef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -83,6 +83,7 @@ account.failed.ssl=連線伺服器時發生了 SSL 錯誤。可能網站證書 account.failed.dns=連線伺服器時發生了 SSL 錯誤。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。 account.failed.wrong_account=登入了錯誤的帳戶 account.friend=好友列表 +account.friend.unsupported=你的認證伺服器不支援好友功能。 account.storage.read_only=帳戶檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改帳戶。 account.hmcl.hint=你需要點擊「登入」按鈕,並在開啟的網頁中完成登入 account.injector.add=新增認證伺服器 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 dda35c1d745..86f8b2e4475 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -84,7 +84,7 @@ account.failed.ssl=连接服务器时发生了 SSL 错误。可能网站证书 account.failed.dns=连接服务器时发生了 SSL 错误。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 account.failed.wrong_account=登录了错误的账户 account.friend=好友列表 -account.friend.unsupported=你的外置登录提供商不支持好友列表功能。 +account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 account.hmcl.hint=你需要点击“登录”按钮,并在弹出的网页中完成登录。 account.injector.add=添加认证服务器 From 1c271197178737c6a83e5f03fe4909bee8f80f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:09:00 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E7=85=9E=E8=A7=81=E7=A5=B8=E7=BB=9D?= =?UTF-8?q?=E6=96=AD=EF=BC=8C=E4=BB=A4=E9=A3=8E=E9=9B=A8=E4=B9=9F=E5=90=8C?= =?UTF-8?q?=E7=94=98=EF=BC=8C=E4=BD=91=E5=B0=94=E5=9B=BD=E6=B3=B0=E6=83=A0?= =?UTF-8?q?=E6=B3=BD=E6=B0=91=E5=AE=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/account/FriendListPage.java | 128 +++++++++++++++++- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + 5 files changed, 133 insertions(+), 2 deletions(-) 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 30f4964b0c2..0eadde7bd28 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -100,6 +100,7 @@ public enum SVG { PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 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.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"), PACKAGE2_FILL("M11 21.725v-9.15L3 7.95v8.025q0 .55.2625 1T4 17.7l7 4.025Zm2 0L20 17.7q.475-.275.7375-.725t.2625-1V7.95l-8 4.625v9.15Zm3.975-13.75 2.95-1.725L13 2.275Q12.525 2 12 2t-1 .275L9.025 3.4l7.95 4.575ZM12 10.85l2.975-1.7L7.05 4.55l-3 1.725L12 10.85Z"), PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"), + PERSON_ADD("M18 14v-3h-3V9h3V6h2v3h3v2h-3v3zM6.175 10.825Q5 9.65 5 8t1.175-2.825T9 4t2.825 1.175T13 8t-1.175 2.825T9 12t-2.825-1.175M1 20v-2.8q0-.85.438-1.562T2.6 14.55q1.55-.775 3.15-1.162T9 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T17 17.2V20zm2-2h12v-.8q0-.275-.137-.5t-.363-.35q-1.35-.675-2.725-1.012T9 15t-2.775.338T3.5 16.35q-.225.125-.363.35T3 17.2zm7.413-8.587Q11 8.825 11 8t-.587-1.412T9 6t-1.412.588T7 8t.588 1.413T9 10t1.413-.587M9 18"), PUBLIC("M12 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 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"), REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"), REDO("M9.9 19q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-2.6-2.6L15 4l5 5l-5 5l-1.4-1.4l2.6-2.6H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H17v2z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index 4dc92237996..fcd67318022 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -17,16 +17,140 @@ */ package org.jackhuang.hmcl.ui.account; -import javafx.scene.layout.Region; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ListCell; +import javafx.scene.control.Skin; +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.friend.FriendControl; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.ToolbarListPageSkin; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -public class FriendListPage extends Region { +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class FriendListPage extends DecoratorAnimatedPage implements DecoratorPage { private final Account account; private final FriendControl control; + private final TabHeader tab; + private final TabHeader.Tab friendPage = new TabHeader.Tab<>("friendPage"); + private final TransitionPane transitionPane = new TransitionPane(); + + @SuppressWarnings("unused") + private ChangeListener instanceChangeListenerHolder; + + + private ReadOnlyObjectWrapper state = null; + public FriendListPage(Account account, FriendControl control) { this.account = account; this.control = control; + + friendPage.setNodeSupplier(Right::new); + tab = new TabHeader(transitionPane, friendPage); + tab.select(friendPage); + + BorderPane left = new BorderPane(); + FXUtils.setLimitWidth(left, 200); + VBox.setVgrow(left, Priority.ALWAYS); + setLeft(left); + + AdvancedListBox sideBar = new AdvancedListBox().addNavigationDrawerTab(tab, friendPage, i18n("account.friend"), SVG.GROUP); + left.setTop(sideBar); + + AdvancedListBox toolbar = new AdvancedListBox().addNavigationDrawerItem(i18n("account.friend.add"), SVG.PERSON_ADD, this::onAddFriend); + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); + left.setBottom(toolbar); + + setCenter(transitionPane); + } + + private void onAddFriend() { + + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + if (state == null) + state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.friend") + " - " + account.getProfileName())); + return state; + } + + private final class Right extends ListPageBase { + private Right() { + super(); + + refresh(); + } + + private void refresh() { + setItems(FXCollections.emptyObservableList()); + setLoading(true); + setFailedReason(null); + + Task.supplyAsync(control::getFriendList).whenComplete(Schedulers.javafx(), (result, exception) -> { + setLoading(false); + if (exception != null) { + LOG.warning("Failed to get friend list: " + exception); + setFailedReason(i18n("account.friend.failed")); + } else { + getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.FRIEND)).collect(Collectors.toList())); + getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).collect(Collectors.toList())); + getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).collect(Collectors.toList())); + } + + }).start(); + } + + @Override + protected Skin createDefaultSkin() { + return new RightSkin(this); + } + } + + private final class RightSkin extends ToolbarListPageSkin { + public RightSkin(Right skinnable) { + super(skinnable); + + listView.setCellFactory(x -> new FriendListCell()); + } + + @Override + protected List initializeToolbar(Right skinnable) { + return List.of(); + } + } + + private final class FriendListCell extends ListCell { + + } + + private record FriendListItem(String profileId, String name, FriendStatus status) { + } + + private enum FriendStatus { + FRIEND, + INCOMING, + OUTGOING } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9d90d051f51..96618f4ef80 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -80,6 +80,8 @@ account.failed.ssl=An SSL error occurred while connecting to the server. Please account.failed.dns=An SSL error occurred while connecting to the server. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service. account.failed.wrong_account=You have logged in to the wrong account. account.friend=Friend List +account.friend.add=Add Friend +account.friend.failed=Failed to load friend list. Please click here to retry. account.friend.unsupported=Your authentication server does not support friend feature. account.storage.read_only=The account file was saved by a newer version of HMCL. The current version of HMCL cannot modify accounts. account.hmcl.hint=You need to click "Log in" and complete the process in the opened browser window. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1685a45baef..c17eb8761b6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -83,6 +83,8 @@ account.failed.ssl=連線伺服器時發生了 SSL 錯誤。可能網站證書 account.failed.dns=連線伺服器時發生了 SSL 錯誤。可能是 DNS 解析有誤。請嘗試更換 DNS 伺服器或使用代理服務。 account.failed.wrong_account=登入了錯誤的帳戶 account.friend=好友列表 +account.friend.add=添加好友 +account.friend.failed=載入好友列表失敗,按一下此處重試。 account.friend.unsupported=你的認證伺服器不支援好友功能。 account.storage.read_only=帳戶檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改帳戶。 account.hmcl.hint=你需要點擊「登入」按鈕,並在開啟的網頁中完成登入 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 86f8b2e4475..2b08a890f7f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -84,6 +84,8 @@ account.failed.ssl=连接服务器时发生了 SSL 错误。可能网站证书 account.failed.dns=连接服务器时发生了 SSL 错误。可能是 DNS 解析有误。请尝试更换 DNS 服务器或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 account.failed.wrong_account=登录了错误的账户 account.friend=好友列表 +account.friend.add=添加好友 +account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 account.hmcl.hint=你需要点击“登录”按钮,并在弹出的网页中完成登录。 From 15519482e313d84290c47f25c43d70192c4d2915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:12:55 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E8=BF=99=E5=A4=A9=E5=9C=B0=E4=B8=87?= =?UTF-8?q?=E8=88=AC=EF=BC=8C=E5=8A=BF=E5=BF=85=E4=B8=8E=E6=88=91=E4=B9=85?= =?UTF-8?q?=E5=91=A8=E6=97=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/account/FriendListPage.java | 11 +++++++++++ .../jackhuang/hmcl/game/friend/FriendResponse.java | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index fcd67318022..7723a8af15a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -27,6 +27,7 @@ import javafx.scene.control.Skin; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.game.friend.FriendControl; @@ -38,6 +39,7 @@ import org.jackhuang.hmcl.ui.ToolbarListPageSkin; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -142,7 +144,16 @@ protected List initializeToolbar(Right skinnable) { } private final class FriendListCell extends ListCell { + private final Region graphic; + public FriendListCell() { + BorderPane root = new BorderPane(); + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8, 8, 8, 0)); + + RipplerContainer container = new RipplerContainer(root); + this.graphic = container; + } } private record FriendListItem(String profileId, String name, FriendStatus status) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java index 3122ad1ec6d..2eaa885fdee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendResponse.java @@ -23,8 +23,7 @@ import java.util.List; @JsonSerializable -public record FriendResponse - (@SerializedName("friends") List friends, - @SerializedName("incomingRequests") List incomingRequests, - @SerializedName("outgoingRequests") List outgoingRequests) { +public record FriendResponse(@SerializedName("friends") List friends, + @SerializedName("incomingRequests") List incomingRequests, + @SerializedName("outgoingRequests") List outgoingRequests) { } From e5ac1dd8413f997df62169873d4dcb56ff34cda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:24:10 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E6=8B=A9=E5=A4=A9=E5=91=BD=E9=99=8D?= =?UTF-8?q?=E4=B8=8B=E5=87=A1=EF=BC=8C=E7=A0=B4=E5=B0=98=E5=AF=B0=EF=BC=8C?= =?UTF-8?q?=E9=95=BF=E8=99=B9=E7=AB=9F=E8=B4=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/account/AccountListItemSkin.java | 21 ++++----- .../hmcl/ui/account/FriendListPage.java | 46 +++++++++++++++++-- .../hmcl/auth/microsoft/MicrosoftSession.java | 4 -- 4 files changed, 51 insertions(+), 21 deletions(-) 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 0eadde7bd28..3574b9ebee7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -101,6 +101,7 @@ public enum SVG { PACKAGE2_FILL("M11 21.725v-9.15L3 7.95v8.025q0 .55.2625 1T4 17.7l7 4.025Zm2 0L20 17.7q.475-.275.7375-.725t.2625-1V7.95l-8 4.625v9.15Zm3.975-13.75 2.95-1.725L13 2.275Q12.525 2 12 2t-1 .275L9.025 3.4l7.95 4.575ZM12 10.85l2.975-1.7L7.05 4.55l-3 1.725L12 10.85Z"), PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"), PERSON_ADD("M18 14v-3h-3V9h3V6h2v3h3v2h-3v3zM6.175 10.825Q5 9.65 5 8t1.175-2.825T9 4t2.825 1.175T13 8t-1.175 2.825T9 12t-2.825-1.175M1 20v-2.8q0-.85.438-1.562T2.6 14.55q1.55-.775 3.15-1.162T9 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T17 17.2V20zm2-2h12v-.8q0-.275-.137-.5t-.363-.35q-1.35-.675-2.725-1.012T9 15t-2.775.338T3.5 16.35q-.225.125-.363.35T3 17.2zm7.413-8.587Q11 8.825 11 8t-.587-1.412T9 6t-1.412.588T7 8t.588 1.413T9 10t1.413-.587M9 18"), + PERSON_CANCEL("M17.4 13L16 11.6l2.075-2.1L16 7.425L17.4 6l2.1 2.1L21.575 6L23 7.425L20.9 9.5l2.1 2.1l-1.425 1.4l-2.075-2.075zM6.175 10.825Q5 9.65 5 8t1.175-2.825T9 4t2.825 1.175T13 8t-1.175 2.825T9 12t-2.825-1.175M1 20v-2.8q0-.85.438-1.562T2.6 14.55q1.55-.775 3.15-1.162T9 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T17 17.2V20zm2-2h12v-.8q0-.275-.137-.5t-.363-.35q-1.35-.675-2.725-1.012T9 15t-2.775.338T3.5 16.35q-.225.125-.363.35T3 17.2zm7.413-8.587Q11 8.825 11 8t-.587-1.412T9 6t-1.412.588T7 8t.588 1.413T9 10t1.413-.587M9 18"), PUBLIC("M12 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 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"), REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"), REDO("M9.9 19q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-2.6-2.6L15 4l5 5l-5 5l-1.4-1.4l2.6-2.6H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H17v2z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index 79e66348a00..79997437cf6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -25,12 +25,10 @@ import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.canvas.Canvas; -import javafx.scene.control.Label; import javafx.scene.control.SkinBase; import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; @@ -44,6 +42,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.javafx.BindingMapping; @@ -73,21 +72,18 @@ public AccountListItemSkin(AccountListItem skinnable) { Canvas canvas = new Canvas(32, 32); TexturesLoader.bindAvatar(canvas, skinnable.getAccount()); - Label title = new Label(); - title.getStyleClass().add("title"); - title.textProperty().bind(skinnable.titleProperty()); - Label subtitle = new Label(); - subtitle.getStyleClass().add("subtitle"); - subtitle.textProperty().bind(skinnable.subtitleProperty()); + TwoLineListItem item = new TwoLineListItem(); + item.getFirstLine().getStyleClass().clear(); + BorderPane.setAlignment(item, Pos.CENTER); + + item.titleProperty().bind(skinnable.titleProperty()); + item.subtitleProperty().bind(skinnable.subtitleProperty()); if (skinnable.getAccount() instanceof AuthlibInjectorAccount) { Tooltip tooltip = new Tooltip(); AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer(); tooltip.textProperty().bind(BindingMapping.of(server, AuthlibInjectorServer::toString)); - FXUtils.installSlowTooltip(subtitle, tooltip); + FXUtils.installSlowTooltip(item, tooltip); } - VBox item = new VBox(title, subtitle); - item.getStyleClass().add("two-line-list-item"); - BorderPane.setAlignment(item, Pos.CENTER); center.getChildren().setAll(canvas, item); root.setCenter(center); @@ -162,6 +158,7 @@ public AccountListItemSkin(AccountListItem skinnable) { right.getChildren().add(spinnerUpload); SpinnerPane spinnerFriend = new SpinnerPane(); + spinnerFriend.getStyleClass().add("small-spinner-pane"); JFXButton btnFriend = FXUtils.newToggleButton4(SVG.GROUP); btnFriend.disableProperty().bind(Bindings.not(skinnable.canAddFriend())); FXUtils.installFastTooltip(btnFriend, i18n("account.friend")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index 7723a8af15a..e749229cd18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -22,14 +22,14 @@ import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.canvas.Canvas; import javafx.scene.control.ListCell; import javafx.scene.control.Skin; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -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.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -41,6 +41,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -112,10 +113,13 @@ private void refresh() { Task.supplyAsync(control::getFriendList).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); + if (exception != null) { - LOG.warning("Failed to get friend list: " + exception); + LOG.warning("Failed to get friend list" + exception); setFailedReason(i18n("account.friend.failed")); } else { + LOG.info("Received friend list" + result); + getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.FRIEND)).collect(Collectors.toList())); getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).collect(Collectors.toList())); getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).collect(Collectors.toList())); @@ -145,6 +149,8 @@ protected List initializeToolbar(Right skinnable) { private final class FriendListCell extends ListCell { private final Region graphic; + private final Canvas avatar = new Canvas(32, 32); + private final TwoLineListItem twoLineListItem = new TwoLineListItem(); public FriendListCell() { BorderPane root = new BorderPane(); @@ -153,6 +159,36 @@ public FriendListCell() { RipplerContainer container = new RipplerContainer(root); this.graphic = container; + + HBox center = new HBox(); + center.setMouseTransparent(true); + center.setPrefWidth(Region.USE_PREF_SIZE); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + center.getChildren().setAll(avatar, twoLineListItem); + root.setCenter(center); + + HBox right = new HBox(); + + var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + + right.getChildren().addAll(deleteButton); + right.setAlignment(Pos.CENTER_RIGHT); + root.setRight(right); + } + + @Override + protected void updateItem(FriendListItem item, boolean empty) { + var currentItem = getItem(); + + super.updateItem(item, empty); + + if (currentItem == getItem()) return; + + TexturesLoader.bindAvatar(avatar, account); + twoLineListItem.setTitle(item.name()); + twoLineListItem.setSubtitle(getClass().getName()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java index 9a292fe8d79..2aa094710d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java @@ -43,10 +43,6 @@ public MicrosoftSession(String tokenType, String accessToken, long notAfter, Str if (accessToken != null) Logger.registerAccessToken(accessToken); } - public String getAuthorization() { - return String.format("%s %s", tokenType(), accessToken()); - } - /// Returns whether the stored session contains a usable Minecraft profile name. public boolean hasProfileName() { return profile != null && StringUtils.isNotBlank(profile.name()); From 35d5f9cbdf978c18edafb64b810c43217887f3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:22:23 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E5=8E=9F=E5=A4=A7=E9=81=93=EF=BC=8C?= =?UTF-8?q?=E8=99=9A=E7=9B=88=E4=B8=8D=E6=95=8C=E4=BA=BA=E5=BF=83=EF=BC=8C?= =?UTF-8?q?=E8=A6=86=E5=8F=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/FriendListPage.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index e749229cd18..2ce96237a56 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -20,7 +20,6 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -107,7 +106,7 @@ private Right() { } private void refresh() { - setItems(FXCollections.emptyObservableList()); + getItems().clear(); setLoading(true); setFailedReason(null); @@ -184,11 +183,19 @@ protected void updateItem(FriendListItem item, boolean empty) { super.updateItem(item, empty); - if (currentItem == getItem()) return; + TexturesLoader.unbindAvatar(avatar); - TexturesLoader.bindAvatar(avatar, account); - twoLineListItem.setTitle(item.name()); - twoLineListItem.setSubtitle(getClass().getName()); + if (empty || item == null) { + this.setGraphic(null); + } else { + this.setGraphic(this.graphic); + + if (currentItem == getItem()) return; + + TexturesLoader.bindAvatar(avatar, account); + twoLineListItem.setTitle(item.name()); + twoLineListItem.setSubtitle(getClass().getName()); + } } } @@ -196,8 +203,6 @@ private record FriendListItem(String profileId, String name, FriendStatus status } private enum FriendStatus { - FRIEND, - INCOMING, - OUTGOING + FRIEND, INCOMING, OUTGOING } } From aa3f9ab648b18c07b6b5785ee214181a8e79cfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:48:55 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E4=B9=98=E8=88=9F=E8=80=8C=E6=83=91?= =?UTF-8?q?=E8=80=85=EF=BC=8C=E5=A4=A7=E6=B5=B7=E6=BC=AB=E6=97=A0=E7=96=86?= =?UTF-8?q?=EF=BC=8C=E9=9D=92=E5=A4=A9=E9=9A=BE=E5=86=8D=E4=B8=8A=E4=BD=95?= =?UTF-8?q?=E5=A4=84=E5=BD=92=E4=B9=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/TexturesLoader.java | 13 ++++++++ .../hmcl/ui/account/FriendListPage.java | 31 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 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 09500345e23..159ea77767c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -38,6 +38,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.io.IOException; @@ -322,4 +323,16 @@ public static void unbindAvatar(Canvas canvas) { } } // ==== + + public static Optional getMicrosoftAccountSkin(String uuid) throws IOException, ServerResponseMalformedException { + return YggdrasilService.getTextures( + HttpRequest.GET("https://api.minecraftservices.com/minecraft/profile/" + uuid) + .getJson(CompleteGameProfile.class)).map(e -> e.get(TextureType.SKIN)); + } + + public static Optional getYggdrasilAccountSkin(String uuid, String url) throws IOException, ServerResponseMalformedException { + return YggdrasilService.getTextures( + HttpRequest.GET(url + uuid) + .getJson(CompleteGameProfile.class)).map(e -> e.get(TextureType.SKIN)); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index 2ce96237a56..05aac9453b2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -28,6 +28,11 @@ import javafx.scene.control.Skin; import javafx.scene.layout.*; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; +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.YggdrasilService; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; @@ -45,6 +50,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -154,7 +160,7 @@ private final class FriendListCell extends ListCell { public FriendListCell() { BorderPane root = new BorderPane(); root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8, 8, 8, 0)); + root.setPadding(new Insets(8, 8, 8, 8)); RipplerContainer container = new RipplerContainer(root); this.graphic = container; @@ -192,9 +198,28 @@ protected void updateItem(FriendListItem item, boolean empty) { if (currentItem == getItem()) return; - TexturesLoader.bindAvatar(avatar, account); + UUID uuid = UUID.fromString(item.profileId()); + + try { + CompleteGameProfile profile = null; + + if (account instanceof YggdrasilAccount yggdrasilAccount) { + profile = yggdrasilAccount.getYggdrasilService().getCompleteGameProfile(uuid).orElseThrow(); + } else if (account instanceof MicrosoftAccount microsoftAccount) { + profile = microsoftAccount.getService().getCompleteGameProfile(uuid).orElseThrow(); + } + + var texture = YggdrasilService.getTextures(profile).map(it -> it.get(TextureType.SKIN)).orElseThrow(); + var loadedTexture = TexturesLoader.loadTexture(texture); + TexturesLoader.drawAvatar(avatar, loadedTexture.image()); + } catch (Throwable e) { + LOG.warning("Failed to load skin", e); + var skin = TexturesLoader.getDefaultSkin(uuid); + TexturesLoader.drawAvatar(avatar, skin.image()); + } + twoLineListItem.setTitle(item.name()); - twoLineListItem.setSubtitle(getClass().getName()); + twoLineListItem.setSubtitle(toString()); } } } From 64f520b3339799aa213a5a6cd6e287166788e004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:55:00 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E8=BF=B7=E4=B9=8E=E4=BA=91=E6=A2=A6?= =?UTF-8?q?=E8=80=85=EF=BC=8C=E5=BF=85=E6=8C=87=E5=BE=80=E5=8D=97=E6=96=B9?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E8=AF=86=E8=A5=BF=E4=B8=9C=E4=B8=8E=E4=B9=8B?= =?UTF-8?q?=E5=85=B1=E5=A4=9C=E8=88=AA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/account/FriendListPage.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index 05aac9453b2..a2d4c2431b5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -51,6 +51,7 @@ import java.util.List; import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -59,6 +60,7 @@ public final class FriendListPage extends DecoratorAnimatedPage implements DecoratorPage { private final Account account; private final FriendControl control; + private static final Pattern REGEX = Pattern.compile("([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{12})"); private final TabHeader tab; private final TabHeader.Tab friendPage = new TabHeader.Tab<>("friendPage"); @@ -198,7 +200,8 @@ protected void updateItem(FriendListItem item, boolean empty) { if (currentItem == getItem()) return; - UUID uuid = UUID.fromString(item.profileId()); + String formatted = REGEX.matcher(item.profileId()).replaceAll("$1-$2-$3-$4-$5"); + var uuid = UUID.fromString(formatted); try { CompleteGameProfile profile = null; From 5cfcda54b86afbfc5b9eb2ceaf099442631b8eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:06:00 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E6=88=91=E8=BA=AB=E8=99=BD=E6=B8=BA?= =?UTF-8?q?=E8=8C=AB=EF=BC=8C=E6=9A=97=E8=BF=B9=E6=97=A0=E5=BD=B7=E5=BE=A8?= =?UTF-8?q?=EF=BC=8C=E4=BC=8F=E6=B3=A2=E6=B8=A1=E6=B5=B7=E5=94=AF=E8=A7=81?= =?UTF-8?q?=E5=8D=83=E7=A7=8B=E7=99=BE=E4=BB=A3=E4=BF=AF=E4=BB=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/game/TexturesLoader.java | 2 +- .../jackhuang/hmcl/ui/account/FriendListPage.java | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 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 159ea77767c..11d416a9c1b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -91,7 +91,7 @@ private static Path getTexturePath(Texture texture) { return TEXTURES_DIR.resolve(prefix).resolve(hash); } - public static LoadedTexture loadTexture(Texture texture) throws Throwable { + public static LoadedTexture loadTexture(Texture texture) throws Exception { if (StringUtils.isBlank(texture.url())) { throw new IOException("Texture url is empty"); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java index a2d4c2431b5..8d9bf79f16a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java @@ -203,7 +203,7 @@ protected void updateItem(FriendListItem item, boolean empty) { String formatted = REGEX.matcher(item.profileId()).replaceAll("$1-$2-$3-$4-$5"); var uuid = UUID.fromString(formatted); - try { + Task.supplyAsync(() -> { CompleteGameProfile profile = null; if (account instanceof YggdrasilAccount yggdrasilAccount) { @@ -213,13 +213,16 @@ protected void updateItem(FriendListItem item, boolean empty) { } var texture = YggdrasilService.getTextures(profile).map(it -> it.get(TextureType.SKIN)).orElseThrow(); - var loadedTexture = TexturesLoader.loadTexture(texture); - TexturesLoader.drawAvatar(avatar, loadedTexture.image()); - } catch (Throwable e) { - LOG.warning("Failed to load skin", e); + return TexturesLoader.loadTexture(texture); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + TexturesLoader.drawAvatar(avatar, result.image()); + return; + } else LOG.warning("Failed to load skin", exception); + var skin = TexturesLoader.getDefaultSkin(uuid); TexturesLoader.drawAvatar(avatar, skin.image()); - } + }).start(); twoLineListItem.setTitle(item.name()); twoLineListItem.setSubtitle(toString());