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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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/25] =?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()); From 0e101ef16c726308de921b6ef4362a97d6e22479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:44:57 +0800 Subject: [PATCH 12/25] =?UTF-8?q?=E6=96=99=E5=85=B5=E5=AE=B6=E5=B8=B8?= =?UTF-8?q?=E9=AA=8C=EF=BC=8C=E5=88=99=E5=A4=AA=E5=8F=B2=E5=A4=9A=E8=A1=8C?= =?UTF-8?q?=E5=8D=A0=EF=BC=8C=E6=88=91=E7=AE=97=E4=B8=96=E9=97=B4=E5=88=B0?= =?UTF-8?q?=E5=BA=95=E4=B8=8D=E5=AE=B9=E5=9C=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/AccountListItemSkin.java | 70 +++-- .../hmcl/ui/account/FriendListPage.java | 239 ------------------ .../ui/account/friend/FriendListCell.java | 109 ++++++++ .../ui/account/friend/FriendListItem.java | 21 ++ .../ui/account/friend/FriendListPage.java | 72 ++++++ .../ui/account/friend/FriendListPageSkin.java | 36 +++ .../hmcl/ui/account/friend/FriendPage.java | 86 +++++++ .../hmcl/ui/account/friend/FriendStatus.java | 22 ++ 8 files changed, 379 insertions(+), 276 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendStatus.java 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 04465c5c6be..c34905e3b5b 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 @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.account.friend.FriendPage; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.io.ResponseCodeException; @@ -128,15 +129,13 @@ public AccountListItemSkin(AccountListItem skinnable) { } btnRefresh.setOnAction(e -> { spinnerRefresh.showSpinner(); - skinnable.refreshAsync() - .whenComplete(Schedulers.javafx(), ex -> { - spinnerRefresh.hideSpinner(); - - if (ex != null) { - Controllers.showToast(Accounts.localizeErrorMessage(ex)); - } - }) - .start(); + skinnable.refreshAsync().whenComplete(Schedulers.javafx(), ex -> { + spinnerRefresh.hideSpinner(); + + if (ex != null) { + Controllers.showToast(Accounts.localizeErrorMessage(ex)); + } + }).start(); }); FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")); spinnerRefresh.setContent(btnRefresh); @@ -148,9 +147,7 @@ public AccountListItemSkin(AccountListItem skinnable) { Task uploadTask = skinnable.uploadSkin(); if (uploadTask != null) { spinnerUpload.showSpinner(); - uploadTask - .whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner()) - .start(); + uploadTask.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner()).start(); } }); FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")); @@ -170,30 +167,30 @@ public AccountListItemSkin(AccountListItem skinnable) { 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)); - }); + skinnable.refreshAsync().whenComplete(exception -> { + if (exception != null) { + Controllers.showToast(Accounts.localizeErrorMessage(exception)); + spinnerFriend.hideSpinner(); + return; + } + + if (account instanceof MicrosoftAccount microsoftAccount) { + Platform.runLater(() -> Controllers.navigate(new FriendPage(account, microsoftAccount))); + } else if (account instanceof YggdrasilAccount yggdrasilAccount) { + try { + yggdrasilAccount.getFriendList(); + Platform.runLater(() -> Controllers.navigate(new FriendPage(account, yggdrasilAccount))); + } catch (IOException e) { + Platform.runLater(() -> { + if (e instanceof ResponseCodeException responseCodeException && responseCodeException.getResponseCode() == 404) { + Controllers.dialog(i18n("account.friend.unsupported")); } - } - }).thenRunAsync(Schedulers.javafx(), spinnerFriend::hideSpinner).start(); + + Controllers.showToast(Accounts.localizeErrorMessage(e)); + }); + } + } + }).thenRunAsync(Schedulers.javafx(), spinnerFriend::hideSpinner).start(); }); right.getChildren().add(spinnerFriend); @@ -222,8 +219,7 @@ private static void moveAccount(AccountListItem skinnable) { Accounts.getAccounts().remove(account); if (account.isPortable()) { account.setPortable(false); - if (!Accounts.getAccounts().contains(account)) - Accounts.getAccounts().add(account); + if (!Accounts.getAccounts().contains(account)) Accounts.getAccounts().add(account); } else { account.setPortable(true); if (!Accounts.getAccounts().contains(account)) { 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 deleted file mode 100644 index 8d9bf79f16a..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/FriendListPage.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.value.ChangeListener; -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.*; -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; -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.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; - -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; -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 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"); - 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() { - getItems().clear(); - 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 { - 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())); - } - - }).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 final Region graphic; - private final Canvas avatar = new Canvas(32, 32); - private final TwoLineListItem twoLineListItem = new TwoLineListItem(); - - public FriendListCell() { - BorderPane root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8, 8, 8, 8)); - - 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); - - TexturesLoader.unbindAvatar(avatar); - - if (empty || item == null) { - this.setGraphic(null); - } else { - this.setGraphic(this.graphic); - - if (currentItem == getItem()) return; - - String formatted = REGEX.matcher(item.profileId()).replaceAll("$1-$2-$3-$4-$5"); - var uuid = UUID.fromString(formatted); - - Task.supplyAsync(() -> { - 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(); - 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()); - } - } - } - - private record FriendListItem(String profileId, String name, FriendStatus status) { - } - - private enum FriendStatus { - FRIEND, INCOMING, OUTGOING - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java new file mode 100644 index 00000000000..ee58f915daa --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -0,0 +1,109 @@ +/* + * 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.friend; + +import com.jfoenix.controls.JFXListView; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.canvas.Canvas; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +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.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.MDListCell; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +import java.util.UUID; +import java.util.regex.Pattern; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class FriendListCell extends MDListCell { + private final Account account; + private final Canvas avatar = new Canvas(32, 32); + private final TwoLineListItem twoLineListItem = new TwoLineListItem(); + 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})"); + + public FriendListCell(JFXListView listView, Account account) { + super(listView); + + this.account = account; + + BorderPane root = new BorderPane(); + root.setPadding(new Insets(8, 8, 8, 8)); + + 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); + + getContainer().getChildren().setAll(root); + } + + @Override + protected void updateControl(FriendListItem item, boolean empty) { + String formatted = REGEX.matcher(item.profileId()).replaceAll("$1-$2-$3-$4-$5"); + var uuid = UUID.fromString(formatted); + + Task.supplyAsync(() -> { + 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(); + 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()); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java new file mode 100644 index 00000000000..007b36face3 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java @@ -0,0 +1,21 @@ +/* + * 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.friend; + +public record FriendListItem(String profileId, String name, FriendStatus status) { +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java new file mode 100644 index 00000000000..6a446ca9467 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -0,0 +1,72 @@ +/* + * 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.friend; + +import javafx.scene.control.Skin; +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.ListPageBase; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class FriendListPage extends ListPageBase { + private final Account account; + private final FriendControl control; + + public FriendListPage(Account account, FriendControl control) { + super(); + this.account = account; + this.control = control; + + refresh(); + } + + private void refresh() { + getItems().clear(); + 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 { + LOG.info("Received friend list" + result); + + getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); + getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); + getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); + } + + }).start(); + } + + @Override + protected Skin createDefaultSkin() { + return new FriendListPageSkin(this); + } + + public Account getAccount() { + return account; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java new file mode 100644 index 00000000000..d81e877bcb7 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java @@ -0,0 +1,36 @@ +/* + * 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.friend; + +import javafx.scene.Node; +import org.jackhuang.hmcl.ui.ToolbarListPageSkin; + +import java.util.List; + +public final class FriendListPageSkin extends ToolbarListPageSkin { + public FriendListPageSkin(FriendListPage skinnable) { + super(skinnable); + + listView.setCellFactory(x -> new FriendListCell(super.listView, skinnable.getAccount())); + } + + @Override + protected List initializeToolbar(FriendListPage skinnable) { + return List.of(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java new file mode 100644 index 00000000000..e840c467f67 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -0,0 +1,86 @@ +/* + * 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.friend; + +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; +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.friend.FriendControl; +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.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public final class FriendPage 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 FriendPage(Account account, FriendControl control) { + this.account = account; + this.control = control; + + friendPage.setNodeSupplier(() -> new FriendListPage(account, control)); + 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; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendStatus.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendStatus.java new file mode 100644 index 00000000000..f53ef531899 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendStatus.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.ui.account.friend; + +public enum FriendStatus { + NORMAL, INCOMING, OUTGOING +} From d837fb2c75832e662921a98cc6053f7dacee8566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:17:48 +0800 Subject: [PATCH 13/25] =?UTF-8?q?=E6=B5=91=E5=A4=A9=E7=A9=BA=E5=9E=82?= =?UTF-8?q?=E5=BD=B1=EF=BC=8C=E5=8F=B8=E8=8B=8D=E7=94=9F=E6=97=A0=E5=8F=AF?= =?UTF-8?q?=E6=8E=A2=EF=BC=8C=E7=9C=8B=E9=80=A0=E5=8C=96=E4=B8=8D=E5=81=9C?= =?UTF-8?q?=E6=B0=B8=E5=BE=AA=E7=8E=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/friend/FriendListCell.java | 34 +++++++++++++++---- .../ui/account/friend/FriendListPage.java | 4 +++ .../ui/account/friend/FriendListPageSkin.java | 2 +- .../resources/assets/lang/I18N.properties | 3 ++ .../resources/assets/lang/I18N_zh.properties | 3 ++ .../assets/lang/I18N_zh_CN.properties | 3 ++ .../AuthlibInjectorAccount.java | 2 ++ .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 6 ++++ .../hmcl/auth/yggdrasil/YggdrasilService.java | 19 +++++++---- .../hmcl/game/friend/EnumUpdateType.java | 23 +++++++++++++ .../hmcl/game/friend/FriendControl.java | 15 ++++++++ .../hmcl/game/friend/FriendUpdateRequst.java | 28 +++++++++++++++ 12 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumUpdateType.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index ee58f915daa..9caa85c06c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -31,28 +31,32 @@ 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; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.MDListCell; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import java.util.UUID; -import java.util.regex.Pattern; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class FriendListCell extends MDListCell { private final Account account; + private final FriendControl control; private final Canvas avatar = new Canvas(32, 32); private final TwoLineListItem twoLineListItem = new TwoLineListItem(); - 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})"); - public FriendListCell(JFXListView listView, Account account) { + public FriendListCell(JFXListView listView, Account account, FriendControl control) { super(listView); this.account = account; + this.control = control; BorderPane root = new BorderPane(); root.setPadding(new Insets(8, 8, 8, 8)); @@ -68,10 +72,29 @@ public FriendListCell(JFXListView listView, Account account) { HBox right = new HBox(); - var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + var copyButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + copyButton.setOnAction(event -> FXUtils.copyText(control.toUuidWithDashes(getItem().profileId()))); + FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); + right.getChildren().addAll(copyButton); + var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + deleteButton.setOnAction(event -> { + Controllers.confirm(i18n("account.friend.delete.confirm"), null, () -> { + Task.runAsync(() -> control.deleteFriend(control.toUuidWithoutDashes(getItem().profileId()))).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + LOG.warning("Failed to delete friend", exception); + Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); + return; + } + listView.getItems().remove(getItem()); + }); + + }, null); + + }); right.getChildren().addAll(deleteButton); right.setAlignment(Pos.CENTER_RIGHT); + root.setRight(right); getContainer().getChildren().setAll(root); @@ -79,8 +102,7 @@ public FriendListCell(JFXListView listView, Account account) { @Override protected void updateControl(FriendListItem item, boolean empty) { - String formatted = REGEX.matcher(item.profileId()).replaceAll("$1-$2-$3-$4-$5"); - var uuid = UUID.fromString(formatted); + var uuid = UUID.fromString(control.toUuidWithDashes(item.profileId())); Task.supplyAsync(() -> { CompleteGameProfile profile = null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index 6a446ca9467..b26877eada3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -69,4 +69,8 @@ protected Skin createDefaultSkin() { public Account getAccount() { return account; } + + public FriendControl getControl() { + return control; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java index d81e877bcb7..c5a6e73d7fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java @@ -26,7 +26,7 @@ public final class FriendListPageSkin extends ToolbarListPageSkin new FriendListCell(super.listView, skinnable.getAccount())); + listView.setCellFactory(x -> new FriendListCell(super.listView, skinnable.getAccount(), skinnable.getControl())); } @Override diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index cd14e2c4f6f..925d053ebd1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -81,6 +81,9 @@ account.failed.dns=An SSL error occurred while connecting to the server. DNS res account.failed.wrong_account=You have logged in to the wrong account. account.friend=Friend List account.friend.add=Add Friend +account.friend.delete=Delete Friend +account.friend.delete.failed=Failed to delete friend +account.friend.delete.confirm=Are you sure you want to delete friend "%s"? This operation cannot be undone! 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. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 2792d6c3fc1..7eefc22c0f7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -84,6 +84,9 @@ account.failed.dns=連線伺服器時發生了 SSL 錯誤。可能是 DNS 解析 account.failed.wrong_account=登入了錯誤的帳戶 account.friend=好友列表 account.friend.add=添加好友 +account.friend.delete=刪除好友 +account.friend.delete.failed=刪除好友失敗 +account.friend.delete.confirm=確認要刪除好友 "%s" 吗?此操作無法恢復! account.friend.failed=載入好友列表失敗,按一下此處重試。 account.friend.unsupported=你的認證伺服器不支援好友功能。 account.storage.read_only=帳戶檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改帳戶。 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 5ac921c79de..ea765f81806 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -85,6 +85,9 @@ account.failed.dns=连接服务器时发生了 SSL 错误。可能是 DNS 解析 account.failed.wrong_account=登录了错误的账户 account.friend=好友列表 account.friend.add=添加好友 +account.friend.delete=删除好友 +account.friend.delete.failed=删除好友失败 +account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无法撤销! account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 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 403cc60f797..699f96f452f 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 @@ -185,4 +185,6 @@ public static Set getUploadableTextures(CompleteGameProfile profile } return unmodifiableSet(result); } + + } 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 ec4004c2392..3206d10a4a6 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 @@ -238,6 +238,12 @@ public FriendResponse getFriendList() throws IOException { return service.getFriendList(session.accessToken()); } + + @Override + public void deleteFriend(String uuid) throws IOException { + service.deleteFriend(session.accessToken(), uuid); + } + @Override public String toString() { return "YggdrasilAccount[accountID=" + getAccountID() + ", profileID=" + profileID 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 1afb1eeaae0..410aa2b9e17 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 @@ -19,7 +19,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; import org.glavo.uuid.UUIDs; import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; @@ -241,6 +240,18 @@ public FriendResponse getFriendList(String accessToken) throws IOException { return JsonUtils.fromNonNullJson(NetworkUtils.readFullyAsString(request), FriendResponse.class); } + public void deleteFriend(String accessToken, String uuid) throws IOException { + var url = provider.getFriendsURL().toString(); + HttpURLConnection request = HttpRequest.PUT(url) + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").createConnection(); + + if (request.getResponseCode() != 200) { + throw new ResponseCodeException(url, request.getResponseCode()); + } + } + private static String request(URI uri, Object payload) throws AuthenticationException { try { if (payload == null) @@ -253,11 +264,7 @@ private static String request(URI uri, Object payload) throws AuthenticationExce } private static T fromJson(String text, Class typeOfT) throws ServerResponseMalformedException { - try { - return GSON.fromJson(text, typeOfT); - } catch (JsonParseException e) { - throw new ServerResponseMalformedException(text, e); - } + return null; } private final static class TextureResponse { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumUpdateType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumUpdateType.java new file mode 100644 index 00000000000..244e6c0d81f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumUpdateType.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.friend; + +public enum EnumUpdateType { + ADD, + REMOVE, +} 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 af1062113a2..1ebe92ac982 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 @@ -18,7 +18,22 @@ package org.jackhuang.hmcl.game.friend; import java.io.IOException; +import java.util.regex.Pattern; public interface FriendControl { + 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})"); + FriendResponse getFriendList() throws IOException; + + void deleteFriend(String uuid) throws IOException; + + default String toUuidWithDashes(String uuidMayWithoutDashes) { + if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; + else return REGEX.matcher(uuidMayWithoutDashes).replaceAll("$1-$2-$3-$4-$5"); + } + + default String toUuidWithoutDashes(String uuidMayWithDashes) { + if (!uuidMayWithDashes.contains("-")) return uuidMayWithDashes; + else return uuidMayWithDashes.replace("-", ""); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java new file mode 100644 index 00000000000..97f128e3107 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java @@ -0,0 +1,28 @@ +/* + * 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 FriendUpdateRequst( + @SerializedName("profileId") String profileID, + @SerializedName("updateType") EnumUpdateType updateType +) { +} From 724de61d095c18e4694d46ee464635bbaf3299be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:35:08 +0800 Subject: [PATCH 14/25] =?UTF-8?q?=20=E6=95=85=E6=8B=A9=E6=97=A5=E6=8B=9C?= =?UTF-8?q?=E5=9D=9B=EF=BC=8C=E7=BA=B5=E6=A8=AA=E6=97=A0=E5=B8=B8=E7=81=BE?= =?UTF-8?q?=E6=82=A3,=E8=B0=93=E6=88=91=E4=B8=8D=E6=85=88=E4=B8=8D?= =?UTF-8?q?=E4=BB=81=E4=B8=8D=E7=A5=A5=E8=A8=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/friend/FriendListCell.java | 3 ++- .../hmcl/auth/microsoft/MicrosoftAccount.java | 6 ++++++ .../hmcl/auth/microsoft/MicrosoftService.java | 21 +++++++++++++++++-- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 5 +++-- .../hmcl/auth/yggdrasil/YggdrasilService.java | 14 +++++++------ .../hmcl/game/friend/FriendControl.java | 4 ++-- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index 9caa85c06c4..34aef2d1b21 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -31,6 +31,7 @@ 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.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -80,7 +81,7 @@ public FriendListCell(JFXListView listView, Account account, Fri var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); deleteButton.setOnAction(event -> { Controllers.confirm(i18n("account.friend.delete.confirm"), null, () -> { - Task.runAsync(() -> control.deleteFriend(control.toUuidWithoutDashes(getItem().profileId()))).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(getItem().profileId()), EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to delete friend", exception); Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); 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 64a32e254cb..2d81331021c 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,6 +23,7 @@ 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.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.javafx.BindingMapping; @@ -179,6 +180,11 @@ public FriendResponse getFriendList() throws IOException { return service.getFriendList(session.accessToken()); } + @Override + public void updateFriend(String uuid, EnumUpdateType updateType) throws IOException { + service.updateFriend(session.accessToken(), uuid, updateType); + } + @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 ee167e89c28..4ccb8274c4a 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,9 @@ 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.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendResponse; +import org.jackhuang.hmcl.game.friend.FriendUpdateRequst; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; @@ -314,18 +316,33 @@ 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") + var url = "https://api.minecraftservices.com/friends"; + HttpURLConnection request = HttpRequest.GET(url) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").createConnection(); if (request.getResponseCode() != 200) { - throw new ResponseCodeException("https://api.minecraftservices.com/friends", request.getResponseCode()); + throw new ResponseCodeException(url, request.getResponseCode()); } return JsonUtils.fromNonNullJson(NetworkUtils.readFullyAsString(request), FriendResponse.class); } + public void updateFriend(String accessToken, String uuid, EnumUpdateType updateType) throws IOException { + var url = "https://api.minecraftservices.com/friends"; + + HttpURLConnection request = HttpRequest.PUT(url) + .json(new FriendUpdateRequst(uuid, updateType)) + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").createConnection(); + + if (request.getResponseCode() != 200) { + throw new ResponseCodeException(url, request.getResponseCode()); + } + } + 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 3206d10a4a6..488a5f48a40 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,6 +21,7 @@ import javafx.beans.binding.ObjectBinding; import org.glavo.uuid.UUIDs; import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -240,8 +241,8 @@ public FriendResponse getFriendList() throws IOException { @Override - public void deleteFriend(String uuid) throws IOException { - service.deleteFriend(session.accessToken(), uuid); + public void updateFriend(String uuid, EnumUpdateType updateType) throws IOException { + service.updateFriend(session.accessToken(), uuid, updateType); } @Override 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 410aa2b9e17..888418b192e 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,9 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendResponse; +import org.jackhuang.hmcl.game.friend.FriendUpdateRequst; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; @@ -45,6 +47,7 @@ import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.gson.JsonUtils.fromJson; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class YggdrasilService { @@ -192,7 +195,9 @@ public static Optional> getTextures(CompleteGameProfil throw new ServerResponseMalformedException(e); } TextureResponse texturePayload = fromJson(new String(decodedBinary, UTF_8), TextureResponse.class); - return Optional.ofNullable(texturePayload.textures); + if (texturePayload != null) { + return Optional.ofNullable(texturePayload.textures); + } else return Optional.empty(); } else { return Optional.empty(); } @@ -240,9 +245,10 @@ public FriendResponse getFriendList(String accessToken) throws IOException { return JsonUtils.fromNonNullJson(NetworkUtils.readFullyAsString(request), FriendResponse.class); } - public void deleteFriend(String accessToken, String uuid) throws IOException { + public void updateFriend(String accessToken, String uuid, EnumUpdateType updateType) throws IOException { var url = provider.getFriendsURL().toString(); HttpURLConnection request = HttpRequest.PUT(url) + .json(new FriendUpdateRequst(uuid, updateType)) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").createConnection(); @@ -263,10 +269,6 @@ private static String request(URI uri, Object payload) throws AuthenticationExce } } - private static T fromJson(String text, Class typeOfT) throws ServerResponseMalformedException { - return null; - } - private final static class TextureResponse { public Map textures; } 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 1ebe92ac982..319c00310af 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 @@ -25,8 +25,8 @@ public interface FriendControl { FriendResponse getFriendList() throws IOException; - void deleteFriend(String uuid) throws IOException; - + void updateFriend(String uuid, EnumUpdateType updateType) throws IOException; + default String toUuidWithDashes(String uuidMayWithoutDashes) { if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; else return REGEX.matcher(uuidMayWithoutDashes).replaceAll("$1-$2-$3-$4-$5"); From 762bb9e672dc00897724c8b77b94215348292dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:01:25 +0800 Subject: [PATCH 15/25] =?UTF-8?q?=E5=8F=AB=E8=8D=A7=E6=83=91=E4=BA=89?= =?UTF-8?q?=E6=9D=83=EF=BC=8C=E5=A4=AA=E4=B8=8A=E6=98=8E=E5=A0=82=E7=9A=86?= =?UTF-8?q?=E7=81=AD=EF=BC=8C=E9=BE=99=E5=A4=B4=E5=B0=BD=E5=80=BE=E6=B3=BB?= =?UTF-8?q?=E5=8C=96=E6=9D=80=E7=83=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/friend/FriendListCell.java | 10 +++++++--- .../jackhuang/hmcl/ui/account/friend/FriendPage.java | 5 ++++- HMCL/src/main/resources/assets/lang/I18N.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 ++ .../main/resources/assets/lang/I18N_zh_CN.properties | 2 ++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index 34aef2d1b21..ac5060a6fbe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -73,14 +73,14 @@ public FriendListCell(JFXListView listView, Account account, Fri HBox right = new HBox(); - var copyButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + var copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); copyButton.setOnAction(event -> FXUtils.copyText(control.toUuidWithDashes(getItem().profileId()))); FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); right.getChildren().addAll(copyButton); var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); deleteButton.setOnAction(event -> { - Controllers.confirm(i18n("account.friend.delete.confirm"), null, () -> { + Controllers.confirm(i18n("account.friend.delete.confirm", getItem().name()), null, () -> { Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(getItem().profileId()), EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to delete friend", exception); @@ -103,6 +103,10 @@ public FriendListCell(JFXListView listView, Account account, Fri @Override protected void updateControl(FriendListItem item, boolean empty) { + TexturesLoader.unbindAvatar(avatar); + + if (item == null || empty) return; + var uuid = UUID.fromString(control.toUuidWithDashes(item.profileId())); Task.supplyAsync(() -> { @@ -127,6 +131,6 @@ protected void updateControl(FriendListItem item, boolean empty) { }).start(); twoLineListItem.setTitle(item.name()); - twoLineListItem.setSubtitle(toString()); +// twoLineListItem.setSubtitle(toString()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index e840c467f67..329b86412cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -26,6 +26,7 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.game.friend.FriendControl; +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; @@ -74,7 +75,9 @@ public FriendPage(Account account, FriendControl control) { } private void onAddFriend() { - + Controllers.prompt(i18n("TODO"), (e,a) -> { + Controllers.dialog("TODO"); + }, null); } @Override diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 925d053ebd1..58f96e1cc00 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -84,6 +84,7 @@ account.friend.add=Add Friend account.friend.delete=Delete Friend account.friend.delete.failed=Failed to delete friend account.friend.delete.confirm=Are you sure you want to delete friend "%s"? This operation cannot be undone! + 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. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7eefc22c0f7..c90abd7c4f6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -87,6 +87,8 @@ account.friend.add=添加好友 account.friend.delete=刪除好友 account.friend.delete.failed=刪除好友失敗 account.friend.delete.confirm=確認要刪除好友 "%s" 吗?此操作無法恢復! +account.friend.incoming=待你接受 +account.friend.outgoing=待對方接受 account.friend.failed=載入好友列表失敗,按一下此處重試。 account.friend.unsupported=你的認證伺服器不支援好友功能。 account.storage.read_only=帳戶檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改帳戶。 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 ea765f81806..8e6fc545665 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -88,6 +88,8 @@ account.friend.add=添加好友 account.friend.delete=删除好友 account.friend.delete.failed=删除好友失败 account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无法撤销! +account.friend.incoming=Pending your acceptance +account.friend.outgoing=Pending the other party's acceptance account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 From 18e59fdd6f21ad9ac2a8e6e9ac33e4df65a170a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:15:02 +0800 Subject: [PATCH 16/25] =?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=A6=82=E4=BD=95=E4=B8=8E=E6=88=91=E4=B9=85?= =?UTF-8?q?=E5=91=A8=E6=97=8B=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/account/friend/FriendListCell.java | 9 +++++++++ HMCL/src/main/resources/assets/lang/I18N.properties | 3 ++- .../src/main/resources/assets/lang/I18N_zh_CN.properties | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index ac5060a6fbe..b9c25e9ff9f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -21,6 +21,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.canvas.Canvas; +import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; @@ -42,6 +43,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import java.util.Locale; import java.util.UUID; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -131,6 +133,13 @@ protected void updateControl(FriendListItem item, boolean empty) { }).start(); twoLineListItem.setTitle(item.name()); + // twoLineListItem.setSubtitle(toString()); + + if (item.status() != FriendStatus.NORMAL) { + var tag = new Label(i18n("account.friend." + item.status().name().toLowerCase(Locale.ROOT))); + tag.getStyleClass().add("tag"); + twoLineListItem.getTags().add(tag); + } } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 58f96e1cc00..deee01695ae 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -84,7 +84,8 @@ account.friend.add=Add Friend account.friend.delete=Delete Friend account.friend.delete.failed=Failed to delete friend account.friend.delete.confirm=Are you sure you want to delete friend "%s"? This operation cannot be undone! - +account.friend.incoming=Pending your acceptance +account.friend.outgoing=Pending the other party's acceptance 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. 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 8e6fc545665..7743ce235e4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -88,8 +88,8 @@ account.friend.add=添加好友 account.friend.delete=删除好友 account.friend.delete.failed=删除好友失败 account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无法撤销! -account.friend.incoming=Pending your acceptance -account.friend.outgoing=Pending the other party's acceptance +account.friend.incoming=待你接受 +account.friend.outgoing=待对方接受 account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 From 9cd0af9b8d133b849bb90b7882e22e9197dd5724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:23:24 +0800 Subject: [PATCH 17/25] =?UTF-8?q?=E6=8B=A9=E5=A4=A9=E5=91=BD=E5=B7=B2?= =?UTF-8?q?=E5=9B=9E=E8=BF=98=EF=BC=8C=E6=9C=9B=E5=A4=A7=E5=8D=83=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E7=A9=B7=E6=98=9F=E6=B1=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthlibInjectorAccount.java | 25 +++-------- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 43 +++++++------------ 2 files changed, 20 insertions(+), 48 deletions(-) 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 699f96f452f..3e1f520967b 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 @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.LaunchOptions; 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; @@ -47,12 +48,7 @@ public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtif this.downloader = downloader; } - public AuthlibInjectorAccount( - AccountID accountID, - AuthlibInjectorServer server, - AuthlibInjectorArtifactProvider downloader, - String username, - YggdrasilSession session) { + public AuthlibInjectorAccount(AccountID accountID, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) { super(accountID, server.getYggdrasilService(), username, session); this.server = server; this.downloader = downloader; @@ -135,10 +131,7 @@ public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo ar @Override public Arguments getLaunchArguments(LaunchOptions options) { - return new Arguments().addJVMArguments( - "-javaagent:" + artifact.location().toString() + "=" + server.getUrl(), - "-Dauthlibinjector.side=client", - "-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8))); + return new Arguments().addJVMArguments("-javaagent:" + artifact.location().toString() + "=" + server.getUrl(), "-Dauthlibinjector.side=client", "-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8))); } } @@ -160,18 +153,12 @@ public AuthlibInjectorServer getServer() { @Override public String toString() { - return new ToStringBuilder(this) - .append("accountID", getAccountID()) - .append("profileID", profileID) - .append("loginName", getLoginName()) - .append("server", getServer().getUrl()) - .toString(); + return new ToStringBuilder(this).append("accountID", getAccountID()).append("profileID", profileID).append("loginName", getLoginName()).append("server", getServer().getUrl()).toString(); } public static Set getUploadableTextures(CompleteGameProfile profile) { String prop = profile.getProperties().get("uploadableTextures"); - if (prop == null) - return emptySet(); + if (prop == null) return emptySet(); Set result = EnumSet.noneOf(TextureType.class); for (String val : prop.split(",")) { val = val.toUpperCase(Locale.ROOT); @@ -185,6 +172,4 @@ public static Set getUploadableTextures(CompleteGameProfile profile } return unmodifiableSet(result); } - - } 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 488a5f48a40..b9b7629eb8e 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 @@ -68,10 +68,7 @@ protected YggdrasilAccount(YggdrasilService service, String loginName, String pa GameProfile characterToSelect = selector.select(service, acquiredSession.availableProfiles()); - session = service.refresh( - acquiredSession.accessToken(), - acquiredSession.clientToken(), - characterToSelect); + session = service.refresh(acquiredSession.accessToken(), acquiredSession.clientToken(), characterToSelect); // response validity has been checked in refresh() } else { session = acquiredSession; @@ -84,6 +81,7 @@ protected YggdrasilAccount(YggdrasilService service, String loginName, String pa } private ObjectBinding> profilePropertiesBinding; + private void addProfilePropertiesListener() { // binding() is thread-safe // hold the binding so that it won't be garbage-collected @@ -123,8 +121,7 @@ public synchronized AuthInfo logIn() throws AuthenticationException { throw e; } } - if (acquiredSession.selectedProfile() == null || - !acquiredSession.selectedProfile().getId().equals(profileID)) { + if (acquiredSession.selectedProfile() == null || !acquiredSession.selectedProfile().getId().equals(profileID)) { throw new ServerResponseMalformedException("Selected profile changed"); } if (!acquiredSession.hasProfileName()) { @@ -150,15 +147,9 @@ public synchronized AuthInfo logInWithPassword(String password) throws Authentic throw new CharacterDeletedException(); } - GameProfile characterToSelect = acquiredSession.availableProfiles().stream() - .filter(charatcer -> charatcer.getId().equals(profileID)) - .findFirst() - .orElseThrow(CharacterDeletedException::new); + GameProfile characterToSelect = acquiredSession.availableProfiles().stream().filter(charatcer -> charatcer.getId().equals(profileID)).findFirst().orElseThrow(CharacterDeletedException::new); - session = service.refresh( - acquiredSession.accessToken(), - acquiredSession.clientToken(), - characterToSelect); + session = service.refresh(acquiredSession.accessToken(), acquiredSession.clientToken(), characterToSelect); } else { if (!acquiredSession.selectedProfile().getId().equals(profileID)) { @@ -192,8 +183,7 @@ public void writeMetadata(JsonObject metadata) { public void writePrivateData(JsonObject privateData) { super.writePrivateData(privateData); session.writePrivateData(privateData); - service.getProfileRepository().getImmediately(profileID).ifPresent(profile -> - privateData.add("profileProperties", JsonUtils.GSON.toJsonTree(profile.getProperties()))); + service.getProfileRepository().getImmediately(profileID).ifPresent(profile -> privateData.add("profileProperties", JsonUtils.GSON.toJsonTree(profile.getProperties()))); } public YggdrasilService getYggdrasilService() { @@ -208,15 +198,14 @@ public void clearCache() { @Override public ObjectBinding>> getTextures() { - return BindingMapping.of(service.getProfileRepository().binding(getProfileID())) - .map(profile -> profile.flatMap(it -> { - try { - return YggdrasilService.getTextures(it); - } catch (ServerResponseMalformedException e) { - LOG.warning("Failed to parse texture payload", e); - return Optional.empty(); - } - })); + return BindingMapping.of(service.getProfileRepository().binding(getProfileID())).map(profile -> profile.flatMap(it -> { + try { + return YggdrasilService.getTextures(it); + } catch (ServerResponseMalformedException e) { + LOG.warning("Failed to parse texture payload", e); + return Optional.empty(); + } + })); } @@ -239,7 +228,6 @@ public FriendResponse getFriendList() throws IOException { return service.getFriendList(session.accessToken()); } - @Override public void updateFriend(String uuid, EnumUpdateType updateType) throws IOException { service.updateFriend(session.accessToken(), uuid, updateType); @@ -247,7 +235,6 @@ public void updateFriend(String uuid, EnumUpdateType updateType) throws IOExcept @Override public String toString() { - return "YggdrasilAccount[accountID=" + getAccountID() + ", profileID=" + profileID - + ", loginName=" + loginName + "]"; + return "YggdrasilAccount[accountID=" + getAccountID() + ", profileID=" + profileID + ", loginName=" + loginName + "]"; } } From bac0e48b2fa06284927ef78ab629d5836fef832d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:05:20 +0800 Subject: [PATCH 18/25] =?UTF-8?q?=E5=8E=9F=E6=97=A5=E6=9C=88=EF=BC=8C?= =?UTF-8?q?=E4=BA=98=E5=8F=A4=E4=B8=8D=E4=B8=BA=E4=BA=BA=E5=BF=83=EF=BC=8C?= =?UTF-8?q?=E7=AD=96=E5=8F=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/AccountListItemSkin.java | 1 + .../ui/account/friend/FriendListCell.java | 21 ++--------- .../ui/account/friend/FriendListPage.java | 36 +++++++++++++++++++ .../ui/account/friend/FriendListPageSkin.java | 2 +- .../hmcl/ui/account/friend/FriendPage.java | 14 ++++++-- .../hmcl/auth/microsoft/MicrosoftService.java | 2 +- .../hmcl/auth/yggdrasil/YggdrasilService.java | 2 +- .../jackhuang/hmcl/util/io/HttpRequest.java | 5 +++ 8 files changed, 60 insertions(+), 23 deletions(-) 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 c34905e3b5b..16c345ee7f2 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 @@ -184,6 +184,7 @@ public AccountListItemSkin(AccountListItem skinnable) { Platform.runLater(() -> { if (e instanceof ResponseCodeException responseCodeException && responseCodeException.getResponseCode() == 404) { Controllers.dialog(i18n("account.friend.unsupported")); + return; } Controllers.showToast(Accounts.localizeErrorMessage(e)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index b9c25e9ff9f..12745d2e651 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -32,15 +32,12 @@ 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.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.MDListCell; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import java.util.Locale; @@ -55,7 +52,7 @@ public final class FriendListCell extends MDListCell { private final Canvas avatar = new Canvas(32, 32); private final TwoLineListItem twoLineListItem = new TwoLineListItem(); - public FriendListCell(JFXListView listView, Account account, FriendControl control) { + public FriendListCell(JFXListView listView, Account account, FriendControl control, FriendListPage friendListPage) { super(listView); this.account = account; @@ -81,20 +78,7 @@ public FriendListCell(JFXListView listView, Account account, Fri right.getChildren().addAll(copyButton); var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); - deleteButton.setOnAction(event -> { - Controllers.confirm(i18n("account.friend.delete.confirm", getItem().name()), null, () -> { - Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(getItem().profileId()), EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - LOG.warning("Failed to delete friend", exception); - Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); - return; - } - listView.getItems().remove(getItem()); - }); - - }, null); - - }); + deleteButton.setOnAction(event -> friendListPage.tryDeleteFriend(getItem())); right.getChildren().addAll(deleteButton); right.setAlignment(Pos.CENTER_RIGHT); @@ -136,6 +120,7 @@ protected void updateControl(FriendListItem item, boolean empty) { // twoLineListItem.setSubtitle(toString()); + twoLineListItem.getTags().clear(); if (item.status() != FriendStatus.NORMAL) { var tag = new Label(i18n("account.friend." + item.status().name().toLowerCase(Locale.ROOT))); tag.getStyleClass().add("tag"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index b26877eada3..83540f7ecbb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -17,12 +17,18 @@ */ package org.jackhuang.hmcl.ui.account.friend; +import com.jfoenix.controls.JFXButton; import javafx.scene.control.Skin; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -73,4 +79,34 @@ public Account getAccount() { public FriendControl getControl() { return control; } + + public void tryDeleteFriend(FriendListItem item) { + SpinnerPane spinnerPane = new SpinnerPane(); + JFXButton btnOk = new JFXButton(i18n("button.ok")); + + btnOk.setOnAction(action -> { + spinnerPane.showSpinner(); + Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { + spinnerPane.hideSpinner(); + if (exception != null) { + LOG.warning("Failed to delete friend", exception); + fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); + return; + } + getItems().remove(item); + }).start(); + }); + + btnOk.getStyleClass().add("dialog-accept"); + spinnerPane.setContent(btnOk); + + + var dialog = new MessageDialogPane.Builder(i18n("account.friend.delete.confirm", item.name()), i18n("message.question"), MessageDialogPane.MessageType.QUESTION) + .addAction(spinnerPane) + .addCancel(null) + .build(); + + Controllers.dialog(dialog); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java index c5a6e73d7fb..96cf3b4cade 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java @@ -26,7 +26,7 @@ public final class FriendListPageSkin extends ToolbarListPageSkin new FriendListCell(super.listView, skinnable.getAccount(), skinnable.getControl())); + listView.setCellFactory(x -> new FriendListCell(super.listView, skinnable.getAccount(), skinnable.getControl(), skinnable)); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index 329b86412cb..18dec4a516b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -25,7 +25,10 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -36,6 +39,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class FriendPage extends DecoratorAnimatedPage implements DecoratorPage { private final Account account; @@ -75,8 +79,14 @@ public FriendPage(Account account, FriendControl control) { } private void onAddFriend() { - Controllers.prompt(i18n("TODO"), (e,a) -> { - Controllers.dialog("TODO"); + Controllers.prompt(i18n("account.friend.add"), (name, resultHandler) -> { + Task.runAsync(() -> control.updateFriend(name, EnumUpdateType.ADD)).whenComplete(Schedulers.javafx(), e -> { + if (e == null) resultHandler.resolve(); + else { + LOG.warning("Failed to add friend", e); + resultHandler.reject(e.getMessage()); + } + }).start(); }, null); } 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 4ccb8274c4a..312b2863bd5 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 @@ -333,7 +333,7 @@ public void updateFriend(String accessToken, String uuid, EnumUpdateType updateT var url = "https://api.minecraftservices.com/friends"; HttpURLConnection request = HttpRequest.PUT(url) - .json(new FriendUpdateRequst(uuid, updateType)) + .json(new FriendUpdateRequst(uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").createConnection(); 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 888418b192e..70de58c7279 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 @@ -248,7 +248,7 @@ public FriendResponse getFriendList(String accessToken) throws IOException { public void updateFriend(String accessToken, String uuid, EnumUpdateType updateType) throws IOException { var url = provider.getFriendsURL().toString(); HttpURLConnection request = HttpRequest.PUT(url) - .json(new FriendUpdateRequst(uuid, updateType)) + .json(new FriendUpdateRequst(uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").createConnection(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java index 8431d08ac8b..066076704ef 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.util.io; +import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.task.Schedulers; @@ -188,6 +189,10 @@ public T json(Object payload) throws JsonParseException { return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json"); } + public T json(Object payload, Gson gson) throws JsonParseException { + return string(payload instanceof String ? (String) payload : gson.toJson(payload), "application/json"); + } + public T form(Map params) { return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded"); } From 0823e3b69c92d69da88a2b6db8b9158c3c263f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:29:59 +0800 Subject: [PATCH 19/25] =?UTF-8?q?=E4=B8=83=E6=9C=88=E6=B5=81=E7=81=AB?= =?UTF-8?q?=E9=99=8D=EF=BC=8C=E4=B9=9D=E6=9C=88=E6=8E=88=E8=A1=A3=E8=A3=B3?= =?UTF-8?q?=EF=BC=8C=E6=97=A2=E9=A1=BA=E5=A4=A9=E5=BA=94=E6=97=B6=E8=87=AA?= =?UTF-8?q?=E7=84=B6=E5=9B=9E=E5=93=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/AccountListItemSkin.java | 7 +++-- .../ui/account/friend/FriendListPage.java | 2 +- .../hmcl/ui/account/friend/FriendPage.java | 2 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 5 +-- .../hmcl/auth/microsoft/MicrosoftService.java | 24 +++++--------- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 5 +-- .../hmcl/auth/yggdrasil/YggdrasilService.java | 31 ++++++++----------- .../hmcl/game/friend/FriendControl.java | 5 ++- .../hmcl/game/friend/FriendUpdateRequst.java | 3 +- 9 files changed, 39 insertions(+), 45 deletions(-) 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 16c345ee7f2..034772cc79c 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 @@ -168,9 +168,10 @@ public AccountListItemSkin(AccountListItem skinnable) { var account = skinnable.getAccount(); skinnable.refreshAsync().whenComplete(exception -> { + Platform.runLater(spinnerFriend::hideSpinner); + if (exception != null) { - Controllers.showToast(Accounts.localizeErrorMessage(exception)); - spinnerFriend.hideSpinner(); + Platform.runLater(() -> Controllers.showToast(Accounts.localizeErrorMessage(exception))); return; } @@ -191,7 +192,7 @@ public AccountListItemSkin(AccountListItem skinnable) { }); } } - }).thenRunAsync(Schedulers.javafx(), spinnerFriend::hideSpinner).start(); + }).start(); }); right.getChildren().add(spinnerFriend); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index 83540f7ecbb..def464ca376 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -86,7 +86,7 @@ public void tryDeleteFriend(FriendListItem item) { btnOk.setOnAction(action -> { spinnerPane.showSpinner(); - Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { spinnerPane.hideSpinner(); if (exception != null) { LOG.warning("Failed to delete friend", exception); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index 18dec4a516b..25601f50a60 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -80,7 +80,7 @@ public FriendPage(Account account, FriendControl control) { private void onAddFriend() { Controllers.prompt(i18n("account.friend.add"), (name, resultHandler) -> { - Task.runAsync(() -> control.updateFriend(name, EnumUpdateType.ADD)).whenComplete(Schedulers.javafx(), e -> { + Task.runAsync(() -> control.updateFriend(name, null, EnumUpdateType.ADD)).whenComplete(Schedulers.javafx(), e -> { if (e == null) resultHandler.resolve(); else { LOG.warning("Failed to add friend", e); 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 2d81331021c..74e8db5f65b 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 @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Path; @@ -181,8 +182,8 @@ public FriendResponse getFriendList() throws IOException { } @Override - public void updateFriend(String uuid, EnumUpdateType updateType) throws IOException { - service.updateFriend(session.accessToken(), uuid, updateType); + public void updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + service.updateFriend(session.accessToken(), name, uuid, updateType); } @Override 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 312b2863bd5..f173d51cadb 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 @@ -38,6 +38,7 @@ import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; @@ -317,30 +318,21 @@ private static String request(String url, Object payload) throws AuthenticationE public FriendResponse getFriendList(String accessToken) throws IOException { var url = "https://api.minecraftservices.com/friends"; - HttpURLConnection request = HttpRequest.GET(url) + + return 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); + .accept("application/json").getJson(FriendResponse.class); } - public void updateFriend(String accessToken, String uuid, EnumUpdateType updateType) throws IOException { + public void updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { var url = "https://api.minecraftservices.com/friends"; - HttpURLConnection request = HttpRequest.PUT(url) - .json(new FriendUpdateRequst(uuid, updateType), GSON) + HttpRequest.PUT(url) + .json(new FriendUpdateRequst(name, uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) - .accept("application/json").createConnection(); - - if (request.getResponseCode() != 200) { - throw new ResponseCodeException(url, request.getResponseCode()); - } + .accept("application/json").getJson(MinecraftErrorResponse.class); } public static class XboxAuthorizationException extends AuthenticationException { 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 b9b7629eb8e..8120e57b5e1 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 @@ -26,6 +26,7 @@ import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Path; @@ -229,8 +230,8 @@ public FriendResponse getFriendList() throws IOException { } @Override - public void updateFriend(String uuid, EnumUpdateType updateType) throws IOException { - service.updateFriend(session.accessToken(), uuid, updateType); + public void updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + service.updateFriend(session.accessToken(), name, uuid, updateType); } @Override 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 70de58c7279..526d72ae9a5 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 @@ -27,10 +27,13 @@ import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.game.friend.FriendUpdateRequst; 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.*; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.HttpMultipartRequest; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; @@ -233,29 +236,21 @@ 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) + + return 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); + .accept("application/json").getJson(FriendResponse.class); } - public void updateFriend(String accessToken, String uuid, EnumUpdateType updateType) throws IOException { + public void updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { var url = provider.getFriendsURL().toString(); - HttpURLConnection request = HttpRequest.PUT(url) - .json(new FriendUpdateRequst(uuid, updateType), GSON) + + HttpRequest.PUT(url) + .json(new FriendUpdateRequst(name, uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) - .accept("application/json").createConnection(); - - if (request.getResponseCode() != 200) { - throw new ResponseCodeException(url, request.getResponseCode()); - } + .accept("application/json").getJson(ErrorResponse.class); } private static String request(URI uri, Object payload) throws AuthenticationException { 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 319c00310af..480573effe3 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,9 @@ */ package org.jackhuang.hmcl.game.friend; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; import java.util.regex.Pattern; @@ -25,7 +28,7 @@ public interface FriendControl { FriendResponse getFriendList() throws IOException; - void updateFriend(String uuid, EnumUpdateType updateType) throws IOException; + void updateFriend(@Nullable String name, @Nullable String uuid, @NotNull EnumUpdateType updateType) throws IOException; default String toUuidWithDashes(String uuidMayWithoutDashes) { if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java index 97f128e3107..f281fad7088 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendUpdateRequst.java @@ -22,7 +22,8 @@ @JsonSerializable public record FriendUpdateRequst( - @SerializedName("profileId") String profileID, + @SerializedName("name") String name, + @SerializedName("profileId") String profileId, @SerializedName("updateType") EnumUpdateType updateType ) { } From 9891a1f257ff5dcdf4fbf37925783f9d64586674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:07:56 +0800 Subject: [PATCH 20/25] =?UTF-8?q?=E5=A4=AA=E5=B2=81=E4=BD=90=E9=98=B4?= =?UTF-8?q?=E9=98=B3=EF=BC=8C=E7=BB=93=E5=8E=86=E8=AE=B0=E9=A3=8E=E9=9C=9C?= =?UTF-8?q?=EF=BC=8C=E5=A4=A9=E5=9E=82=E4=B8=87=E8=B1=A1=E5=94=AF=E8=A7=81?= =?UTF-8?q?=E5=8D=83=E7=A7=8B=E7=99=BE=E4=BB=A3=E6=9C=AA=E5=A4=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/setting/Accounts.java | 9 ++------- .../hmcl/ui/account/friend/FriendPage.java | 13 ++++++++++++- HMCL/src/main/resources/assets/lang/I18N.properties | 4 ++++ .../main/resources/assets/lang/I18N_zh.properties | 4 ++++ .../resources/assets/lang/I18N_zh_CN.properties | 4 ++++ .../hmcl/auth/microsoft/MicrosoftAccount.java | 3 ++- .../hmcl/auth/microsoft/MicrosoftService.java | 7 ++++++- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index 713c5d698a2..92b6f1789cd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -45,13 +45,8 @@ import java.nio.file.Paths; import java.util.*; -import static java.util.stream.Collectors.toList; import static javafx.collections.FXCollections.observableArrayList; -import static org.jackhuang.hmcl.setting.SettingsManager.settings; -import static org.jackhuang.hmcl.setting.SettingsManager.getAccountMetadataRecords; -import static org.jackhuang.hmcl.setting.SettingsManager.getAuthlibInjectorServers; -import static org.jackhuang.hmcl.setting.SettingsManager.getUserAccountMetadataRecords; -import static org.jackhuang.hmcl.setting.SettingsManager.userSettings; +import static org.jackhuang.hmcl.setting.SettingsManager.*; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.util.Lang.immutableListOf; import static org.jackhuang.hmcl.util.Lang.mapOf; @@ -492,7 +487,7 @@ private static void removeDanglingAuthlibInjectorAccounts() { .map(AuthlibInjectorAccount.class::cast) .filter(it -> !getAuthlibInjectorServers().contains(it.getServer())) .filter(Accounts::canRemoveAccount) - .collect(toList()) + .toList() .forEach(accounts::remove); } // ==== diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index 25601f50a60..349ff950d4c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -25,8 +25,10 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftService; import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; +import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; @@ -37,6 +39,10 @@ import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.ResponseCodeException; + +import java.util.Locale; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -84,7 +90,12 @@ private void onAddFriend() { if (e == null) resultHandler.resolve(); else { LOG.warning("Failed to add friend", e); - resultHandler.reject(e.getMessage()); + if (e.getCause() instanceof ResponseCodeException cause && cause.getData() != null) { + var errResponse = JsonUtils.fromJson(cause.getData(), MicrosoftService.MinecraftErrorResponse.class); + if (errResponse != null && errResponse.details != null && errResponse.details.status != null) { + resultHandler.reject(i18n("account.failed." + errResponse.details.status.toLowerCase(Locale.ROOT))); + } else resultHandler.reject(Accounts.localizeErrorMessage(e)); + } else resultHandler.reject(Accounts.localizeErrorMessage(e)); } }).start(); }, null); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index deee01695ae..8cf61a8cea1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -64,6 +64,10 @@ account.create.authlibInjector=Add an authlib-injector Account account.email=Email account.empty=No Accounts account.failed=Failed to refresh account. +account.failed.unknown_profile=Name does not exist +account.failed.cannot_add_self=Cannot add yourself as friend. +account.failed.already_friends=Already friends. +account.failed.invite_rejected=User does not have friends enabled or accept invites. account.failed.character_deleted=The player has already been deleted. account.failed.connect_authentication_server=Failed to connect to the authentication server, your network connection may be down. account.failed.connect_injector_server=Failed to connect to the authentication server. Please check your network and make sure you entered the correct URL. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index c90abd7c4f6..05c75533e83 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -63,6 +63,10 @@ account.create.authlibInjector=新增 authlib-injector 帳戶 account.email=電子信箱 account.empty=沒有帳戶 account.failed=帳戶重新整理失敗 +account.failed.unknown_profile=用戶不存在 +account.failed.cannot_add_self=不能添加自己為好友 +account.failed.already_friends=已添加對方為好友 +account.failed.invite_rejected=該用戶未開啟好友功能,或不接受邀請 account.failed.character_deleted=已刪除此角色 account.failed.connect_authentication_server=無法連線至認證伺服器。可能是網路問題,請檢查裝置能否正常上網或使用代理服務。 account.failed.connect_injector_server=無法連線至認證伺服器。可能是網路故障,請檢查裝置能否正常上網、檢查網址是否輸入錯誤或使用代理服務。 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 7743ce235e4..bbbfe5c6da6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -63,6 +63,10 @@ account.create.authlibInjector=添加外置登录账户 (authlib-injector) account.email=邮箱 account.empty=没有账户 account.failed=账户刷新失败 +account.failed.unknown_profile=用户不存在 +account.failed.cannot_add_self=不能添加自己为好友 +account.failed.already_friends=已添加对方为好友 +account.failed.invite_rejected=该用户未开启好友功能,或不接受邀请 account.failed.character_deleted=此角色已被删除 account.failed.connect_authentication_server=无法连接认证服务器。可能是网络问题,请检查设备能否正常上网或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 account.failed.connect_injector_server=无法连接认证服务器。可能是网络问题,请检查设备是否能正常上网、URL 是否输入错误,或使用代理服务。\n你可以点击右上角帮助按钮进行求助。 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 74e8db5f65b..af80d48b4d7 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 @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -158,7 +159,7 @@ public MicrosoftService getService() { } @Override - public ObjectBinding>> getTextures() { + public @NotNull ObjectBinding>> getTextures() { return BindingMapping.of(service.getProfileRepository().binding(getProfileID())) .map(profile -> profile.flatMap(it -> { try { 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 f173d51cadb..a1ba46eeeda 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 @@ -500,12 +500,17 @@ public void validate() throws JsonParseException, TolerableValidationException { } } - private static class MinecraftErrorResponse { + public static class MinecraftErrorResponse { public String path; public String errorType; public String error; public String errorMessage; public String developerMessage; + public Details details; + } + + public static class Details { + public String status; } private static final Gson GSON = new GsonBuilder() From 70aabc7fb12f8f67f62615610113349639d2ab78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:51:49 +0800 Subject: [PATCH 21/25] =?UTF-8?q?=E6=98=9F=E7=A7=BB=E6=96=97=E8=BD=AC?= =?UTF-8?q?=EF=BC=8C=E6=9C=AC=E4=B8=8D=E4=B8=BA=EF=BC=8C=E5=8F=A4=E4=BB=8A?= =?UTF-8?q?=E6=9B=B4=E6=98=93=E4=BA=BA=E4=BA=8B=E8=80=8C=E5=AD=98=E4=BA=A1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/friend/FriendListPage.java | 20 +++++++++++++------ .../ui/account/friend/FriendListPageSkin.java | 7 ++++++- .../hmcl/ui/account/friend/FriendPage.java | 2 +- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/game/friend/FriendResponse.java | 3 ++- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index def464ca376..7c7e7f50c3e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -36,19 +36,23 @@ public final class FriendListPage extends ListPageBase { private final Account account; private final FriendControl control; + private final Runnable onAddFriend; - public FriendListPage(Account account, FriendControl control) { + public FriendListPage(Account account, FriendControl control, Runnable onAddFriend) { super(); + this.account = account; this.control = control; + this.onAddFriend = onAddFriend; refresh(); } - private void refresh() { + public void refresh() { getItems().clear(); setLoading(true); setFailedReason(null); + setOnFailedAction(null); Task.supplyAsync(control::getFriendList).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); @@ -59,11 +63,15 @@ private void refresh() { } else { LOG.info("Received friend list" + result); - getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); - getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); - getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); + if (result.empty()) { + setFailedReason(i18n("account.friend.empty")); + setOnFailedAction(event -> onAddFriend.run()); + } else { + getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); + getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); + getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); + } } - }).start(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java index 96cf3b4cade..48927bdc94d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPageSkin.java @@ -18,10 +18,13 @@ package org.jackhuang.hmcl.ui.account.friend; import javafx.scene.Node; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.ToolbarListPageSkin; import java.util.List; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + public final class FriendListPageSkin extends ToolbarListPageSkin { public FriendListPageSkin(FriendListPage skinnable) { super(skinnable); @@ -31,6 +34,8 @@ public FriendListPageSkin(FriendListPage skinnable) { @Override protected List initializeToolbar(FriendListPage skinnable) { - return List.of(); + return List.of( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + ); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index 349ff950d4c..cea3e4d1cbe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -65,7 +65,7 @@ public FriendPage(Account account, FriendControl control) { this.account = account; this.control = control; - friendPage.setNodeSupplier(() -> new FriendListPage(account, control)); + friendPage.setNodeSupplier(() -> new FriendListPage(account, control, this::onAddFriend)); tab = new TabHeader(transitionPane, friendPage); tab.select(friendPage); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 8cf61a8cea1..508bb06117f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -91,6 +91,7 @@ account.friend.delete.confirm=Are you sure you want to delete friend "%s"? This account.friend.incoming=Pending your acceptance account.friend.outgoing=Pending the other party's acceptance account.friend.failed=Failed to load friend list. Please click here to retry. +account.friend.empty=Friend list is empty. Please click here to add a friend. 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 05c75533e83..5a288b3d98f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -94,6 +94,7 @@ account.friend.delete.confirm=確認要刪除好友 "%s" 吗?此操作無法 account.friend.incoming=待你接受 account.friend.outgoing=待對方接受 account.friend.failed=載入好友列表失敗,按一下此處重試。 +account.friend.empty=好友列表為空,按一下此處添加好友。 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 bbbfe5c6da6..7d649debf1d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -95,6 +95,7 @@ account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无 account.friend.incoming=待你接受 account.friend.outgoing=待对方接受 account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) +account.friend.empty=好友列表为空,点击此处添加好友 account.friend.unsupported=你的认证服务器不支持好友功能。 account.storage.read_only=账户文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改账户。 account.hmcl.hint=你需要点击“登录”按钮,并在弹出的网页中完成登录。 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 2eaa885fdee..72d0ce026c7 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 @@ -25,5 +25,6 @@ @JsonSerializable public record FriendResponse(@SerializedName("friends") List friends, @SerializedName("incomingRequests") List incomingRequests, - @SerializedName("outgoingRequests") List outgoingRequests) { + @SerializedName("outgoingRequests") List outgoingRequests, + @SerializedName("empty") boolean empty) { } From 81333b35b9d99f0b299ccfe3f2e759c5ef4f5a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:16:02 +0800 Subject: [PATCH 22/25] =?UTF-8?q?=E8=A7=88=E4=BA=BA=E5=BF=83=E6=B0=94?= =?UTF-8?q?=E8=B1=A1=EF=BC=8C=E7=A0=B4=E6=80=AA=E5=A5=87=E8=99=9A=E5=A6=84?= =?UTF-8?q?=EF=BC=8C=E7=9B=9B=E8=A1=B0=E7=BB=88=E5=9B=9E=E8=BD=AC=E5=90=84?= =?UTF-8?q?=E8=87=AA=E7=9B=B8=E5=82=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/setting/Accounts.java | 3 ++ .../main/java/org/jackhuang/hmcl/ui/SVG.java | 2 + .../hmcl/ui/account/CreateAccountPane.java | 7 +-- .../ui/account/friend/FriendListCell.java | 2 +- .../ui/account/friend/FriendListPage.java | 31 ++++++----- .../hmcl/ui/account/friend/FriendPage.java | 23 +++++---- .../hmcl/ui/construct/UserNameValidator.java | 51 +++++++++++++++++++ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/auth/microsoft/MicrosoftAccount.java | 4 +- .../hmcl/auth/microsoft/MicrosoftService.java | 6 +-- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 4 +- .../hmcl/auth/yggdrasil/YggdrasilService.java | 6 +-- .../hmcl/game/friend/FriendControl.java | 2 +- 15 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/UserNameValidator.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index 92b6f1789cd..c6add5049d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -44,6 +44,7 @@ import java.io.IOException; import java.nio.file.Paths; import java.util.*; +import java.util.regex.Pattern; import static javafx.collections.FXCollections.observableArrayList; import static org.jackhuang.hmcl.setting.SettingsManager.*; @@ -58,6 +59,8 @@ * @author huangyuhui */ public final class Accounts { + public static final Pattern USERNAME_CHECKER_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private Accounts() { } 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 3574b9ebee7..2c5bdd3ddc4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -102,6 +102,8 @@ public enum SVG { 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"), + PERSON_CHECK("M17.55 12L14 8.45l1.425-1.4l2.125 2.125l4.25-4.25l1.4 1.425zM6.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 8"), + PERSON_OFF("M19.775 22.625L17.15 20H4v-2.8q0-.85.438-1.562T5.6 14.55q1.125-.575 2.288-.925t2.362-.525L1.375 4.225L2.8 2.8l18.4 18.4zM6 18h9.15l-3-3H12q-1.4 0-2.775.338T6.5 16.35q-.225.125-.363.35T6 17.2zm12.4-3.45q.725.35 1.15 1.063T20 17.15l-3.35-3.35q.45.175.888.35t.862.4m-4.2-3.2l-1.475-1.475q.575-.225.925-.737T14 8q0-.825-.587-1.412T12 6q-.625 0-1.137.35t-.738.925L8.65 5.8q.575-.85 1.45-1.325T12 4q1.65 0 2.825 1.175T16 8q0 1.025-.475 1.9T14.2 11.35m.95 6.65H6zm-3.725-9.425"), 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/CreateAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java index 7fad875bff0..e866571f48b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java @@ -61,21 +61,18 @@ import java.util.*; import java.util.concurrent.CountDownLatch; -import java.util.regex.Pattern; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static javafx.beans.binding.Bindings.bindContent; import static javafx.beans.binding.Bindings.createBooleanBinding; -import static org.jackhuang.hmcl.setting.SettingsManager.settings; import static org.jackhuang.hmcl.setting.SettingsManager.getAuthlibInjectorServers; +import static org.jackhuang.hmcl.setting.SettingsManager.settings; import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.classPropertyFor; public class CreateAccountPane extends JFXDialogLayout implements DialogAware { - private static final Pattern USERNAME_CHECKER_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); - private boolean showMethodSwitcher; private AccountFactory factory; @@ -247,7 +244,7 @@ private void onAccept() { }).executor(true); }; - if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) { + if (factory instanceof OfflineAccountFactory && username != null && (!Accounts.USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) { Controllers.confirmWithCountdown(i18n("account.methods.offline.name.invalid"), i18n("message.warning"), 10, MessageDialogPane.MessageType.WARNING, doCreate, () -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index 12745d2e651..75ae0f76c2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -77,7 +77,7 @@ public FriendListCell(JFXListView listView, Account account, Fri FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); right.getChildren().addAll(copyButton); - var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_OFF); deleteButton.setOnAction(event -> friendListPage.tryDeleteFriend(getItem())); right.getChildren().addAll(deleteButton); right.setAlignment(Pos.CENTER_RIGHT); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index 7c7e7f50c3e..60eb03ee726 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; +import org.jackhuang.hmcl.game.friend.FriendResponse; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; @@ -49,7 +50,6 @@ public FriendListPage(Account account, FriendControl control, Runnable onAddFrie } public void refresh() { - getItems().clear(); setLoading(true); setFailedReason(null); setOnFailedAction(null); @@ -62,15 +62,7 @@ public void refresh() { setFailedReason(i18n("account.friend.failed")); } else { LOG.info("Received friend list" + result); - - if (result.empty()) { - setFailedReason(i18n("account.friend.empty")); - setOnFailedAction(event -> onAddFriend.run()); - } else { - getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); - getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); - getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); - } + setFriends(result); } }).start(); } @@ -94,15 +86,17 @@ public void tryDeleteFriend(FriendListItem item) { btnOk.setOnAction(action -> { spinnerPane.showSpinner(); - Task.runAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.supplyAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { spinnerPane.hideSpinner(); + if (exception != null) { LOG.warning("Failed to delete friend", exception); fireEvent(new DialogCloseEvent()); Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); return; } - getItems().remove(item); + + setFriends(result); }).start(); }); @@ -117,4 +111,17 @@ public void tryDeleteFriend(FriendListItem item) { Controllers.dialog(dialog); } + + public void setFriends(FriendResponse result) { + getItems().clear(); + + if (result.friends().isEmpty() && result.incomingRequests().isEmpty() && result.outgoingRequests().isEmpty()) { + setFailedReason(i18n("account.friend.empty")); + setOnFailedAction(event -> onAddFriend.run()); + } else { + getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); + getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); + getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index cea3e4d1cbe..db57b0f8e83 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -36,7 +36,9 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jackhuang.hmcl.ui.construct.UserNameValidator; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -52,7 +54,7 @@ public final class FriendPage extends DecoratorAnimatedPage implements Decorator private final FriendControl control; private final TabHeader tab; - private final TabHeader.Tab friendPage = new TabHeader.Tab<>("friendPage"); + private final TabHeader.Tab friendListPage = new TabHeader.Tab<>("friendPage"); private final TransitionPane transitionPane = new TransitionPane(); @SuppressWarnings("unused") @@ -65,16 +67,16 @@ public FriendPage(Account account, FriendControl control) { this.account = account; this.control = control; - friendPage.setNodeSupplier(() -> new FriendListPage(account, control, this::onAddFriend)); - tab = new TabHeader(transitionPane, friendPage); - tab.select(friendPage); + friendListPage.setNodeSupplier(() -> new FriendListPage(account, control, this::onAddFriend)); + tab = new TabHeader(transitionPane, friendListPage); + tab.select(friendListPage); 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); + AdvancedListBox sideBar = new AdvancedListBox().addNavigationDrawerTab(tab, friendListPage, i18n("account.friend"), SVG.GROUP); left.setTop(sideBar); AdvancedListBox toolbar = new AdvancedListBox().addNavigationDrawerItem(i18n("account.friend.add"), SVG.PERSON_ADD, this::onAddFriend); @@ -86,9 +88,12 @@ public FriendPage(Account account, FriendControl control) { private void onAddFriend() { Controllers.prompt(i18n("account.friend.add"), (name, resultHandler) -> { - Task.runAsync(() -> control.updateFriend(name, null, EnumUpdateType.ADD)).whenComplete(Schedulers.javafx(), e -> { - if (e == null) resultHandler.resolve(); - else { + Task.supplyAsync(() -> control.updateFriend(name, null, EnumUpdateType.ADD)).whenComplete(Schedulers.javafx(), (result, e) -> { + if (e == null) { + resultHandler.resolve(); + + friendListPage.getNode().setFriends(result); + } else { LOG.warning("Failed to add friend", e); if (e.getCause() instanceof ResponseCodeException cause && cause.getData() != null) { var errResponse = JsonUtils.fromJson(cause.getData(), MicrosoftService.MinecraftErrorResponse.class); @@ -98,7 +103,7 @@ private void onAddFriend() { } else resultHandler.reject(Accounts.localizeErrorMessage(e)); } }).start(); - }, null); + }, null, new RequiredValidator(), new UserNameValidator()); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/UserNameValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/UserNameValidator.java new file mode 100644 index 00000000000..541e065aa25 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/UserNameValidator.java @@ -0,0 +1,51 @@ +/* + * 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.construct; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.NamedArg; +import javafx.scene.control.TextInputControl; + +import static org.jackhuang.hmcl.setting.Accounts.USERNAME_CHECKER_PATTERN; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class UserNameValidator extends ValidatorBase { + + public UserNameValidator() { + this(i18n("account.invailed_name")); + } + + public UserNameValidator(@NamedArg("message") String message) { + super(message); + } + + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = ((TextInputControl) srcControl.get()); + + var username = textField.getText(); + + hasErrors.set(!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16 || username.length() < 3); + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 508bb06117f..aae44bb17b8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -104,6 +104,7 @@ account.injector.server=Authentication Server account.injector.server.storage.read_only=The authlib-injector server file was saved by a newer version of HMCL. The current version of HMCL cannot modify authentication servers. account.injector.server_url=Server URL account.injector.server_name=Server Name +account.invailed_name=Invalid username account.login=Log in account.login.hint=We never store your password. account.login.skip=Log in offline diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5a288b3d98f..1fa6d5dd5eb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -107,6 +107,7 @@ account.injector.server=認證伺服器 account.injector.server.storage.read_only=認證伺服器檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改認證伺服器。 account.injector.server_url=伺服器位址 account.injector.server_name=伺服器名稱 +account.invailed_name=非法使用者名稱 account.login=登入 account.login.hint=我們不會儲存你的密碼 account.login.skip=跳過重新整理帳戶 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 7d649debf1d..87029c9facc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -108,6 +108,7 @@ account.injector.server=认证服务器 account.injector.server.storage.read_only=认证服务器文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改认证服务器。 account.injector.server_url=服务器地址 account.injector.server_name=服务器名称 +account.invailed_name=非法用户名 account.login=登录 account.login.hint=我们不会保存你的密码 account.login.skip=跳过账户刷新 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 af80d48b4d7..b2f8e733610 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 @@ -183,8 +183,8 @@ public FriendResponse getFriendList() throws IOException { } @Override - public void updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { - service.updateFriend(session.accessToken(), name, uuid, updateType); + public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + return service.updateFriend(session.accessToken(), name, uuid, updateType); } @Override 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 a1ba46eeeda..566b49d4594 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 @@ -325,14 +325,14 @@ public FriendResponse getFriendList(String accessToken) throws IOException { .accept("application/json").getJson(FriendResponse.class); } - public void updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { var url = "https://api.minecraftservices.com/friends"; - HttpRequest.PUT(url) + return HttpRequest.PUT(url) .json(new FriendUpdateRequst(name, uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) - .accept("application/json").getJson(MinecraftErrorResponse.class); + .accept("application/json").getJson(FriendResponse.class); } public static class XboxAuthorizationException extends AuthenticationException { 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 8120e57b5e1..05abf72d097 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 @@ -230,8 +230,8 @@ public FriendResponse getFriendList() throws IOException { } @Override - public void updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { - service.updateFriend(session.accessToken(), name, uuid, updateType); + public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + return service.updateFriend(session.accessToken(), name, uuid, updateType); } @Override 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 526d72ae9a5..06f41798db7 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 @@ -243,14 +243,14 @@ public FriendResponse getFriendList(String accessToken) throws IOException { .accept("application/json").getJson(FriendResponse.class); } - public void updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { var url = provider.getFriendsURL().toString(); - HttpRequest.PUT(url) + return HttpRequest.PUT(url) .json(new FriendUpdateRequst(name, uuid, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) - .accept("application/json").getJson(ErrorResponse.class); + .accept("application/json").getJson(FriendResponse.class); } private static String request(URI uri, Object payload) throws AuthenticationException { 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 480573effe3..1fdc06ee6b2 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 @@ -28,7 +28,7 @@ public interface FriendControl { FriendResponse getFriendList() throws IOException; - void updateFriend(@Nullable String name, @Nullable String uuid, @NotNull EnumUpdateType updateType) throws IOException; + FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, @NotNull EnumUpdateType updateType) throws IOException; default String toUuidWithDashes(String uuidMayWithoutDashes) { if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; From 6cd1b85e5fa2725159eaa1a1c7f29d562549be59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:31:32 +0800 Subject: [PATCH 23/25] =?UTF-8?q?=E9=9D=9E=E5=B8=9D=E5=AD=90=E6=89=80?= =?UTF-8?q?=E5=90=91=EF=BC=8C=E6=AF=8B=E6=98=8E=E6=9B=9C=E5=B0=8A=E7=8E=8B?= =?UTF-8?q?=EF=BC=8C=E8=B0=B4=E5=AF=8C=E8=B4=B5=E6=9C=89=E5=B0=BD=E5=AF=BF?= =?UTF-8?q?=E5=91=BD=E4=B8=8D=E6=98=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/friend/FriendListCell.java | 36 ++++++++++++++----- .../ui/account/friend/FriendListPage.java | 10 +++--- .../resources/assets/lang/I18N.properties | 6 ++++ .../resources/assets/lang/I18N_zh.properties | 6 ++++ .../assets/lang/I18N_zh_CN.properties | 6 ++++ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index 75ae0f76c2a..e85afa133ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.account.friend; +import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -32,6 +33,7 @@ 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.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -52,6 +54,12 @@ public final class FriendListCell extends MDListCell { private final Canvas avatar = new Canvas(32, 32); private final TwoLineListItem twoLineListItem = new TwoLineListItem(); + private final JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); + private final JFXButton deleteButton = FXUtils.newToggleButton4(SVG.PERSON_OFF); + private final JFXButton acceptButton = FXUtils.newToggleButton4(SVG.PERSON_CHECK); + private final JFXButton rejectButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); + private final HBox actions = new HBox(); + public FriendListCell(JFXListView listView, Account account, FriendControl control, FriendListPage friendListPage) { super(listView); @@ -70,19 +78,23 @@ public FriendListCell(JFXListView listView, Account account, Fri center.getChildren().setAll(avatar, twoLineListItem); root.setCenter(center); - HBox right = new HBox(); - var copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); + copyButton.setOnAction(event -> FXUtils.copyText(control.toUuidWithDashes(getItem().profileId()))); FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); - right.getChildren().addAll(copyButton); - var deleteButton = FXUtils.newToggleButton4(SVG.PERSON_OFF); - deleteButton.setOnAction(event -> friendListPage.tryDeleteFriend(getItem())); - right.getChildren().addAll(deleteButton); - right.setAlignment(Pos.CENTER_RIGHT); + deleteButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.delete.confirm"),i18n("account.friend.delete.failed"))); + FXUtils.installFastTooltip(deleteButton, i18n("account.friend.delete")); + + acceptButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.ADD, i18n("account.friend.accept.confirm"),i18n("account.friend.accept.failed"))); + FXUtils.installFastTooltip(acceptButton, i18n("account.friend.accept")); + + rejectButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.reject.confirm"),i18n("account.friend.reject.failed"))); + FXUtils.installFastTooltip(rejectButton, i18n("account.friend.reject")); - root.setRight(right); + actions.setAlignment(Pos.CENTER_RIGHT); + + root.setRight(actions); getContainer().getChildren().setAll(root); } @@ -126,5 +138,13 @@ protected void updateControl(FriendListItem item, boolean empty) { tag.getStyleClass().add("tag"); twoLineListItem.getTags().add(tag); } + + actions.getChildren().setAll(copyButton); + + if (item.status() == FriendStatus.NORMAL) { + actions.getChildren().addAll(deleteButton); + } else if (item.status() == FriendStatus.INCOMING) { + actions.getChildren().addAll(acceptButton, rejectButton); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index 60eb03ee726..2778d24151e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -80,19 +80,19 @@ public FriendControl getControl() { return control; } - public void tryDeleteFriend(FriendListItem item) { + public void confirmUpdateFriend(FriendListItem item, EnumUpdateType updateType, String text, String failedText) { SpinnerPane spinnerPane = new SpinnerPane(); JFXButton btnOk = new JFXButton(i18n("button.ok")); btnOk.setOnAction(action -> { spinnerPane.showSpinner(); - Task.supplyAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, EnumUpdateType.REMOVE)).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.supplyAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, updateType)).whenComplete(Schedulers.javafx(), (result, exception) -> { spinnerPane.hideSpinner(); if (exception != null) { - LOG.warning("Failed to delete friend", exception); + LOG.warning("Failed to update friend", exception); fireEvent(new DialogCloseEvent()); - Controllers.dialog(i18n("account.friend.delete.failed"), null, MessageDialogPane.MessageType.ERROR); + Controllers.dialog(failedText, null, MessageDialogPane.MessageType.ERROR); return; } @@ -104,7 +104,7 @@ public void tryDeleteFriend(FriendListItem item) { spinnerPane.setContent(btnOk); - var dialog = new MessageDialogPane.Builder(i18n("account.friend.delete.confirm", item.name()), i18n("message.question"), MessageDialogPane.MessageType.QUESTION) + var dialog = new MessageDialogPane.Builder(i18n(text, item.name()), i18n("message.question"), MessageDialogPane.MessageType.QUESTION) .addAction(spinnerPane) .addCancel(null) .build(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index aae44bb17b8..5ef73b5fec2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -88,6 +88,12 @@ account.friend.add=Add Friend account.friend.delete=Delete Friend account.friend.delete.failed=Failed to delete friend account.friend.delete.confirm=Are you sure you want to delete friend "%s"? This operation cannot be undone! +account.friend.accept=Accept Friend Request +account.friend.accept.failed=Failed to accept friend request +account.friend.accept.confirm=Are you sure you want to accept friend request "%s"? +account.friend.reject=Reject Friend Request +account.friend.reject.failed=Failed to reject friend request +account.friend.reject.confirm=Are you sure you want to reject friend request "%s"? account.friend.incoming=Pending your acceptance account.friend.outgoing=Pending the other party's acceptance account.friend.failed=Failed to load friend list. Please click here to retry. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1fa6d5dd5eb..d3bc919f531 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -91,6 +91,12 @@ account.friend.add=添加好友 account.friend.delete=刪除好友 account.friend.delete.failed=刪除好友失敗 account.friend.delete.confirm=確認要刪除好友 "%s" 吗?此操作無法恢復! +account.friend.accept=接受好友请求 +account.friend.accept.failed=接受好友请求失敗 +account.friend.accept.confirm=確認要接受好友请求 "%s" 吗? +account.friend.reject=拒绝好友请求 +account.friend.reject.failed=拒绝好友请求失敗 +account.friend.reject.confirm=確認要拒绝好友请求 "%s" 吗? account.friend.incoming=待你接受 account.friend.outgoing=待對方接受 account.friend.failed=載入好友列表失敗,按一下此處重試。 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 87029c9facc..3639d172486 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -92,6 +92,12 @@ account.friend.add=添加好友 account.friend.delete=删除好友 account.friend.delete.failed=删除好友失败 account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无法撤销! +account.friend.accept=接受好友请求 +account.friend.accept.failed=接受好友请求失败 +account.friend.accept.confirm=确认要接受好友请求 “%s” 吗? +account.friend.reject=拒绝好友请求 +account.friend.reject.failed=拒绝好友请求失败 +account.friend.reject.confirm=确认要拒绝好友请求 “%s” 吗? account.friend.incoming=待你接受 account.friend.outgoing=待对方接受 account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) From 8d20f069c13f073893591875c66bcdef5ac26335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:53:32 +0800 Subject: [PATCH 24/25] =?UTF-8?q?=E5=90=AC=E5=90=8E=E4=B8=96=E6=AC=BA?= =?UTF-8?q?=E7=BD=94=EF=BC=8C=E5=92=A8=E5=97=9F=E5=AE=89=E5=8F=AF=E9=95=BF?= =?UTF-8?q?=EF=BC=8C=E6=9E=AF=E8=8D=A3=E6=9C=89=E6=95=B0=E5=94=AF=E8=A7=81?= =?UTF-8?q?=E5=8D=83=E7=A7=8B=E7=99=BE=E4=BB=A3=E8=BE=89=E7=85=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/friend/FriendListCell.java | 16 +-- .../ui/account/friend/FriendListItem.java | 7 +- .../ui/account/friend/FriendListPage.java | 105 +++++++++++++----- .../hmcl/ui/account/friend/FriendPage.java | 2 +- .../resources/assets/lang/I18N.properties | 7 ++ .../resources/assets/lang/I18N_zh.properties | 7 ++ .../assets/lang/I18N_zh_CN.properties | 13 ++- .../AuthlibInjectorProvider.java | 5 + .../hmcl/auth/microsoft/MicrosoftAccount.java | 9 +- .../hmcl/auth/microsoft/MicrosoftService.java | 17 ++- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 10 +- .../auth/yggdrasil/YggdrasilProvider.java | 2 + .../hmcl/auth/yggdrasil/YggdrasilService.java | 18 ++- .../hmcl/game/friend/EnumPresenceStatus.java | 27 +++++ .../hmcl/game/friend/FriendControl.java | 2 + .../hmcl/game/friend/PresenceItem.java | 25 +++++ .../hmcl/game/friend/PresenceRequst.java | 25 +++++ .../hmcl/game/friend/PresenceResponse.java | 25 +++++ 18 files changed, 268 insertions(+), 54 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumPresenceStatus.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceItem.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceRequst.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceResponse.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index e85afa133ff..ac9361a73c7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -33,6 +33,7 @@ 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.EnumPresenceStatus; import org.jackhuang.hmcl.game.friend.EnumUpdateType; import org.jackhuang.hmcl.game.friend.FriendControl; import org.jackhuang.hmcl.task.Schedulers; @@ -41,6 +42,7 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.MDListCell; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.util.i18n.I18n; import java.util.Locale; import java.util.UUID; @@ -54,7 +56,7 @@ public final class FriendListCell extends MDListCell { private final Canvas avatar = new Canvas(32, 32); private final TwoLineListItem twoLineListItem = new TwoLineListItem(); - private final JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); + private final JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY); private final JFXButton deleteButton = FXUtils.newToggleButton4(SVG.PERSON_OFF); private final JFXButton acceptButton = FXUtils.newToggleButton4(SVG.PERSON_CHECK); private final JFXButton rejectButton = FXUtils.newToggleButton4(SVG.PERSON_CANCEL); @@ -78,18 +80,16 @@ public FriendListCell(JFXListView listView, Account account, Fri center.getChildren().setAll(avatar, twoLineListItem); root.setCenter(center); - - copyButton.setOnAction(event -> FXUtils.copyText(control.toUuidWithDashes(getItem().profileId()))); FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); - deleteButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.delete.confirm"),i18n("account.friend.delete.failed"))); + deleteButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.delete.confirm"), i18n("account.friend.delete.failed"))); FXUtils.installFastTooltip(deleteButton, i18n("account.friend.delete")); - acceptButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.ADD, i18n("account.friend.accept.confirm"),i18n("account.friend.accept.failed"))); + acceptButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.ADD, i18n("account.friend.accept.confirm"), i18n("account.friend.accept.failed"))); FXUtils.installFastTooltip(acceptButton, i18n("account.friend.accept")); - rejectButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.reject.confirm"),i18n("account.friend.reject.failed"))); + rejectButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.reject.confirm"), i18n("account.friend.reject.failed"))); FXUtils.installFastTooltip(rejectButton, i18n("account.friend.reject")); actions.setAlignment(Pos.CENTER_RIGHT); @@ -130,7 +130,9 @@ protected void updateControl(FriendListItem item, boolean empty) { twoLineListItem.setTitle(item.name()); -// twoLineListItem.setSubtitle(toString()); + var statusText = i18n("account.friend.presence_status." + (item.presenceStatus() == null ? EnumPresenceStatus.OFFLINE : item.presenceStatus()).name().toLowerCase(Locale.ROOT)); + + twoLineListItem.setSubtitle(i18n("account.friend.presence_status.last_updated", I18n.formatDateTime(item.lastUpdated())) + " / " + statusText); twoLineListItem.getTags().clear(); if (item.status() != FriendStatus.NORMAL) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java index 007b36face3..77b0c49c36d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java @@ -17,5 +17,10 @@ */ package org.jackhuang.hmcl.ui.account.friend; -public record FriendListItem(String profileId, String name, FriendStatus status) { +import org.jackhuang.hmcl.game.friend.EnumPresenceStatus; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; + +public record FriendListItem(String profileId, String name, FriendStatus status, @Nullable EnumPresenceStatus presenceStatus, @Nullable Instant lastUpdated) { } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index 2778d24151e..ee681fc7dc7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -18,19 +18,25 @@ package org.jackhuang.hmcl.ui.account.friend; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXSpinner; import javafx.scene.control.Skin; +import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.game.friend.EnumUpdateType; -import org.jackhuang.hmcl.game.friend.FriendControl; -import org.jackhuang.hmcl.game.friend.FriendResponse; +import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.ListPageBase; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.util.StringUtils; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -54,17 +60,24 @@ public void refresh() { setFailedReason(null); setOnFailedAction(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 { - LOG.info("Received friend list" + result); - setFriends(result); - } - }).start(); + Task.supplyAsync(control::getFriendList) + .thenApplyAsync(result -> pair(result, control.getPresence(EnumPresenceStatus.ONLINE))) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + setLoading(false); + + var friends = result.key(); + var presence = result.value(); + + LOG.info("Received friend list" + friends); + LOG.info("Received presences" + presence); + + if (exception != null) { + LOG.warning("Failed to get friend list" + exception); + setFailedReason(i18n("account.friend.failed")); + } else { + setData(friends, presence); + } + }).start(); } @Override @@ -81,47 +94,79 @@ public FriendControl getControl() { } public void confirmUpdateFriend(FriendListItem item, EnumUpdateType updateType, String text, String failedText) { - SpinnerPane spinnerPane = new SpinnerPane(); JFXButton btnOk = new JFXButton(i18n("button.ok")); + StackPane stackPane = new StackPane(); + + var spinner = new JFXSpinner(); + spinner.getStyleClass().add("small-spinner"); + btnOk.setOnAction(action -> { - spinnerPane.showSpinner(); - Task.supplyAsync(() -> control.updateFriend(control.toUuidWithoutDashes(item.profileId()), null, updateType)).whenComplete(Schedulers.javafx(), (result, exception) -> { - spinnerPane.hideSpinner(); + stackPane.getChildren().setAll(spinner); + Task.supplyAsync(() -> control.updateFriend(null, control.toUuidWithoutDashes(item.profileId()), updateType)).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to update friend", exception); fireEvent(new DialogCloseEvent()); - Controllers.dialog(failedText, null, MessageDialogPane.MessageType.ERROR); + Controllers.dialog(failedText + "\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.ERROR); return; } - setFriends(result); + setData(result, null); + fireEvent(new DialogCloseEvent()); }).start(); }); btnOk.getStyleClass().add("dialog-accept"); - spinnerPane.setContent(btnOk); - + stackPane.getChildren().setAll(btnOk); var dialog = new MessageDialogPane.Builder(i18n(text, item.name()), i18n("message.question"), MessageDialogPane.MessageType.QUESTION) - .addAction(spinnerPane) + .addAction(stackPane) .addCancel(null) .build(); Controllers.dialog(dialog); } - public void setFriends(FriendResponse result) { + private PresenceResponse lastPresence; + + public void setData(FriendResponse friends, PresenceResponse nullablePresence) { + if (nullablePresence != null) lastPresence = nullablePresence; + + var presence = lastPresence; + getItems().clear(); - if (result.friends().isEmpty() && result.incomingRequests().isEmpty() && result.outgoingRequests().isEmpty()) { + if (friends.empty()) { setFailedReason(i18n("account.friend.empty")); setOnFailedAction(event -> onAddFriend.run()); - } else { - getItems().addAll(result.friends().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.NORMAL)).toList()); - getItems().addAll(result.outgoingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.OUTGOING)).toList()); - getItems().addAll(result.incomingRequests().stream().map(it -> new FriendListItem(it.profileId(), it.name(), FriendStatus.INCOMING)).toList()); + return; } + + Map presenceMap = presence == null || presence.presence() == null + ? Map.of() + : presence.presence().stream() + .collect(Collectors.toMap(PresenceItem::profileId, Function.identity())); + + List, FriendStatus>> sources = List.of( + Map.entry(friends.friends(), FriendStatus.NORMAL), + Map.entry(friends.outgoingRequests(), FriendStatus.OUTGOING), + Map.entry(friends.incomingRequests(), FriendStatus.INCOMING) + ); + + sources.forEach(entry -> + getItems().addAll(entry.getKey().stream() + .map(friend -> { + PresenceItem p = presenceMap.get(friend.profileId()); + return new FriendListItem( + friend.profileId(), + friend.name(), + entry.getValue(), + p != null ? p.status() : null, + p != null ? p.lastUpdated() : null + ); + }) + .toList()) + ); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java index db57b0f8e83..039d76b2418 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendPage.java @@ -92,7 +92,7 @@ private void onAddFriend() { if (e == null) { resultHandler.resolve(); - friendListPage.getNode().setFriends(result); + friendListPage.getNode().setData(result, null); } else { LOG.warning("Failed to add friend", e); if (e.getCause() instanceof ResponseCodeException cause && cause.getData() != null) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 5ef73b5fec2..786fa5cb9db 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -96,6 +96,13 @@ account.friend.reject.failed=Failed to reject friend request account.friend.reject.confirm=Are you sure you want to reject friend request "%s"? account.friend.incoming=Pending your acceptance account.friend.outgoing=Pending the other party's acceptance +account.friend.presence_status.last_updated=Last updated: %1s +account.friend.presence_status.offline=Offline +account.friend.presence_status.online=Online +account.friend.presence_status.playing_offline=In a world +account.friend.presence_status.playing_hosted_server=In a world +account.friend.presence_status.playing_realms=In a Minecraft Realms server +account.friend.presence_status.playing_server=In a third-party server account.friend.failed=Failed to load friend list. Please click here to retry. account.friend.empty=Friend list is empty. Please click here to add a friend. account.friend.unsupported=Your authentication server does not support friend feature. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index d3bc919f531..7163b76a78a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -99,6 +99,13 @@ account.friend.reject.failed=拒绝好友请求失敗 account.friend.reject.confirm=確認要拒绝好友请求 "%s" 吗? account.friend.incoming=待你接受 account.friend.outgoing=待對方接受 +account.friend.presence_status.last_updated=最後更新時間: %1s +account.friend.presence_status.offline=離線 +account.friend.presence_status.online=在線 +account.friend.presence_status.playing_offline=在世界內 +account.friend.presence_status.playing_hosted_server=在世界內 +account.friend.presence_status.playing_realms=在 Minecraft Realms 伺服器內 +account.friend.presence_status.playing_server=在第三方 Minecraft 伺服器內 account.friend.failed=載入好友列表失敗,按一下此處重試。 account.friend.empty=好友列表為空,按一下此處添加好友。 account.friend.unsupported=你的認證伺服器不支援好友功能。 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 3639d172486..22280788a1d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -91,15 +91,22 @@ account.friend=好友列表 account.friend.add=添加好友 account.friend.delete=删除好友 account.friend.delete.failed=删除好友失败 -account.friend.delete.confirm=确认要删除好友 “%s” 吗?此操作无法撤销! +account.friend.delete.confirm=确认要删除好友 “%1s” 吗?此操作无法撤销! account.friend.accept=接受好友请求 account.friend.accept.failed=接受好友请求失败 -account.friend.accept.confirm=确认要接受好友请求 “%s” 吗? +account.friend.accept.confirm=确认要接受好友请求 “%1s” 吗? account.friend.reject=拒绝好友请求 account.friend.reject.failed=拒绝好友请求失败 -account.friend.reject.confirm=确认要拒绝好友请求 “%s” 吗? +account.friend.reject.confirm=确认要拒绝好友请求 “%1s” 吗? account.friend.incoming=待你接受 account.friend.outgoing=待对方接受 +account.friend.presence_status.last_updated=最后更新时间: %1s +account.friend.presence_status.offline=离线 +account.friend.presence_status.online=在线 +account.friend.presence_status.playing_offline=在世界内 +account.friend.presence_status.playing_hosted_server=在世界内 +account.friend.presence_status.playing_realms=在 Minecraft Realms 服务器内 +account.friend.presence_status.playing_server=在第三方服务器内 account.friend.failed=[加载好友列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) account.friend.empty=好友列表为空,点击此处添加好友 account.friend.unsupported=你的认证服务器不支持好友功能。 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 5fa4fd4d51e..a26f46fd2b8 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 @@ -59,4 +59,9 @@ public URI getProfilePropertiesURL(UUID uuid) { public URI getFriendsURL() { return URI.create(apiRoot + "minecraftservices/friends"); } + + @Override + public URI getPresenceURL() { + return URI.create(apiRoot + "minecraftservices/presence"); + } } 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 b2f8e733610..6e374a77695 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,9 +23,7 @@ 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.EnumUpdateType; -import org.jackhuang.hmcl.game.friend.FriendControl; -import org.jackhuang.hmcl.game.friend.FriendResponse; +import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -187,6 +185,11 @@ public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, return service.updateFriend(session.accessToken(), name, uuid, updateType); } + @Override + public PresenceResponse getPresence(@NotNull EnumPresenceStatus selfPresence) throws IOException { + return service.getPresence(session.accessToken(), selfPresence); + } + @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 566b49d4594..a7b12df338f 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,13 +31,12 @@ 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.EnumUpdateType; -import org.jackhuang.hmcl.game.friend.FriendResponse; -import org.jackhuang.hmcl.game.friend.FriendUpdateRequst; +import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -46,6 +45,7 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -335,6 +335,16 @@ public FriendResponse updateFriend(String accessToken, @Nullable String name, @N .accept("application/json").getJson(FriendResponse.class); } + public PresenceResponse getPresence(String accessToken, @NotNull EnumPresenceStatus status) throws IOException { + var url = "https://api.minecraftservices.com/presence"; + + return HttpRequest.POST(url) + .json(new PresenceRequst(status), GSON) + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").getJson(PresenceResponse.class); + } + public static class XboxAuthorizationException extends AuthenticationException { private final long errorCode; private final String redirect; @@ -515,6 +525,7 @@ public static class Details { private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) + .registerTypeAdapter(Instant.class, InstantTypeAdapter.INSTANCE) .create(); } 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 05abf72d097..bc6e4f6d0a3 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,11 +21,10 @@ import javafx.beans.binding.ObjectBinding; import org.glavo.uuid.UUIDs; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.game.friend.EnumUpdateType; -import org.jackhuang.hmcl.game.friend.FriendControl; -import org.jackhuang.hmcl.game.friend.FriendResponse; +import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -234,6 +233,11 @@ public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, return service.updateFriend(session.accessToken(), name, uuid, updateType); } + @Override + public PresenceResponse getPresence(@NotNull EnumPresenceStatus status) throws IOException { + return service.getPresence(session.accessToken(), status); + } + @Override public String toString() { return "YggdrasilAccount[accountID=" + getAccountID() + ", profileID=" + profileID + ", loginName=" + loginName + "]"; 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 51a18b5db63..0d85ef12be0 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 @@ -56,4 +56,6 @@ public interface YggdrasilProvider { URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException; URI getFriendsURL(); + + URI getPresenceURL(); } 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 06f41798db7..f8b54aee36c 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,16 +23,16 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; -import org.jackhuang.hmcl.game.friend.EnumUpdateType; -import org.jackhuang.hmcl.game.friend.FriendResponse; -import org.jackhuang.hmcl.game.friend.FriendUpdateRequst; +import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.InstantTypeAdapter; 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.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -41,6 +41,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -253,6 +254,16 @@ public FriendResponse updateFriend(String accessToken, @Nullable String name, @N .accept("application/json").getJson(FriendResponse.class); } + public PresenceResponse getPresence(String accessToken, @NotNull EnumPresenceStatus status) throws IOException { + var url = provider.getPresenceURL().toString(); + + return HttpRequest.POST(url) + .json(new PresenceRequst(status), GSON) + .authorization("Bearer " + accessToken) + .retry(5) + .accept("application/json").getJson(PresenceResponse.class); + } + private static String request(URI uri, Object payload) throws AuthenticationException { try { if (payload == null) @@ -284,6 +295,7 @@ private static class ErrorResponse { private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) + .registerTypeAdapter(Instant.class, InstantTypeAdapter.INSTANCE) .create(); public static final String PURCHASE_URL = "https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumPresenceStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumPresenceStatus.java new file mode 100644 index 00000000000..d59af0c06ad --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/EnumPresenceStatus.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; + +public enum EnumPresenceStatus { + OFFLINE, + ONLINE, + PLAYING_OFFLINE, + PLAYING_HOSTED_SERVER, + PLAYING_REALMS, + PLAYING_SERVER +} 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 1fdc06ee6b2..c2238a46d83 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 @@ -30,6 +30,8 @@ public interface FriendControl { FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, @NotNull EnumUpdateType updateType) throws IOException; + PresenceResponse getPresence(@NotNull EnumPresenceStatus selfPresence) throws IOException; + default String toUuidWithDashes(String uuidMayWithoutDashes) { if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; else return REGEX.matcher(uuidMayWithoutDashes).replaceAll("$1-$2-$3-$4-$5"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceItem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceItem.java new file mode 100644 index 00000000000..646fe95fab1 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceItem.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.friend; + +import com.google.gson.annotations.SerializedName; + +import java.time.Instant; + +public record PresenceItem(@SerializedName("profileId") String profileId, @SerializedName("status") EnumPresenceStatus status , @SerializedName("lastUpdated") Instant lastUpdated) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceRequst.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceRequst.java new file mode 100644 index 00000000000..be4df940e39 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceRequst.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.friend; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.util.gson.JsonSerializable; + +@JsonSerializable +public record PresenceRequst(@SerializedName("status") EnumPresenceStatus status) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceResponse.java new file mode 100644 index 00000000000..bba1ed39ed7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/PresenceResponse.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.friend; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public record PresenceResponse(@SerializedName("presence") List presence) { +} From 12c24be5257aaf05d207486adf499ac674d78f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:08:49 +0800 Subject: [PATCH 25/25] =?UTF-8?q?=E5=A4=A9=E8=A1=8C=E6=9C=89=E5=B8=B8?= =?UTF-8?q?=EF=BC=8C=E6=9C=AC=E4=B8=8D=E4=B8=BA=EF=BC=8C=E5=8F=A4=E4=BB=8A?= =?UTF-8?q?=E6=9B=B4=E6=98=93=E4=BA=BA=E4=BA=8B=E8=80=8C=E5=AD=98=E4=BA=A1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/account/friend/FriendListCell.java | 5 ++--- .../hmcl/ui/account/friend/FriendListItem.java | 3 ++- .../hmcl/ui/account/friend/FriendListPage.java | 2 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 2 +- .../hmcl/auth/microsoft/MicrosoftService.java | 4 ++-- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 2 +- .../hmcl/auth/yggdrasil/YggdrasilService.java | 6 ++++-- .../hmcl/game/friend/FriendControl.java | 16 ++-------------- .../jackhuang/hmcl/game/friend/FriendItem.java | 4 +++- 9 files changed, 18 insertions(+), 26 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java index ac9361a73c7..abbc467f6fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListCell.java @@ -45,7 +45,6 @@ import org.jackhuang.hmcl.util.i18n.I18n; import java.util.Locale; -import java.util.UUID; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -80,7 +79,7 @@ public FriendListCell(JFXListView listView, Account account, Fri center.getChildren().setAll(avatar, twoLineListItem); root.setCenter(center); - copyButton.setOnAction(event -> FXUtils.copyText(control.toUuidWithDashes(getItem().profileId()))); + copyButton.setOnAction(event -> FXUtils.copyText(getItem().profileId().toString())); FXUtils.installFastTooltip(copyButton, i18n("account.copy_uuid")); deleteButton.setOnAction(event -> friendListPage.confirmUpdateFriend(getItem(), EnumUpdateType.REMOVE, i18n("account.friend.delete.confirm"), i18n("account.friend.delete.failed"))); @@ -105,7 +104,7 @@ protected void updateControl(FriendListItem item, boolean empty) { if (item == null || empty) return; - var uuid = UUID.fromString(control.toUuidWithDashes(item.profileId())); + var uuid = item.profileId(); Task.supplyAsync(() -> { CompleteGameProfile profile = null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java index 77b0c49c36d..953bdb5f889 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListItem.java @@ -21,6 +21,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.UUID; -public record FriendListItem(String profileId, String name, FriendStatus status, @Nullable EnumPresenceStatus presenceStatus, @Nullable Instant lastUpdated) { +public record FriendListItem(UUID profileId, String name, FriendStatus status, @Nullable EnumPresenceStatus presenceStatus, @Nullable Instant lastUpdated) { } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java index ee681fc7dc7..c786e7ec1c7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/friend/FriendListPage.java @@ -104,7 +104,7 @@ public void confirmUpdateFriend(FriendListItem item, EnumUpdateType updateType, btnOk.setOnAction(action -> { stackPane.getChildren().setAll(spinner); - Task.supplyAsync(() -> control.updateFriend(null, control.toUuidWithoutDashes(item.profileId()), updateType)).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.supplyAsync(() -> control.updateFriend(null, item.profileId(), updateType)).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to update friend", exception); fireEvent(new DialogCloseEvent()); 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 6e374a77695..e37b2eb8072 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 @@ -181,7 +181,7 @@ public FriendResponse getFriendList() throws IOException { } @Override - public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(@Nullable String name, @Nullable UUID uuid, @NotNull EnumUpdateType updateType) throws IOException { return service.updateFriend(session.accessToken(), name, uuid, updateType); } 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 a7b12df338f..d23ba77058e 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 @@ -325,11 +325,11 @@ public FriendResponse getFriendList(String accessToken) throws IOException { .accept("application/json").getJson(FriendResponse.class); } - public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable UUID uuid, EnumUpdateType updateType) throws IOException { var url = "https://api.minecraftservices.com/friends"; return HttpRequest.PUT(url) - .json(new FriendUpdateRequst(name, uuid, updateType), GSON) + .json(new FriendUpdateRequst(name, uuid != null ? uuid.toString() : null, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").getJson(FriendResponse.class); 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 bc6e4f6d0a3..c4638219c8e 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 @@ -229,7 +229,7 @@ public FriendResponse getFriendList() throws IOException { } @Override - public FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(@Nullable String name, @Nullable UUID uuid, @NotNull EnumUpdateType updateType) throws IOException { return service.updateFriend(session.accessToken(), name, uuid, updateType); } 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 f8b54aee36c..9d17541242e 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 @@ -26,6 +26,7 @@ import org.jackhuang.hmcl.game.friend.*; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.InstantTypeAdapter; +import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.HttpMultipartRequest; @@ -244,11 +245,11 @@ public FriendResponse getFriendList(String accessToken) throws IOException { .accept("application/json").getJson(FriendResponse.class); } - public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable String uuid, EnumUpdateType updateType) throws IOException { + public FriendResponse updateFriend(String accessToken, @Nullable String name, @Nullable UUID uuid, EnumUpdateType updateType) throws IOException { var url = provider.getFriendsURL().toString(); return HttpRequest.PUT(url) - .json(new FriendUpdateRequst(name, uuid, updateType), GSON) + .json(new FriendUpdateRequst(name, uuid != null ? uuid.toString() : null, updateType), GSON) .authorization("Bearer " + accessToken) .retry(5) .accept("application/json").getJson(FriendResponse.class); @@ -295,6 +296,7 @@ private static class ErrorResponse { private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) + .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapter(Instant.class, InstantTypeAdapter.INSTANCE) .create(); 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 c2238a46d83..f60458c12a2 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 @@ -21,24 +21,12 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.util.regex.Pattern; +import java.util.UUID; public interface FriendControl { - 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})"); - FriendResponse getFriendList() throws IOException; - FriendResponse updateFriend(@Nullable String name, @Nullable String uuid, @NotNull EnumUpdateType updateType) throws IOException; + FriendResponse updateFriend(@Nullable String name, @Nullable UUID uuid, @NotNull EnumUpdateType updateType) throws IOException; PresenceResponse getPresence(@NotNull EnumPresenceStatus selfPresence) throws IOException; - - default String toUuidWithDashes(String uuidMayWithoutDashes) { - if (uuidMayWithoutDashes.contains("-")) return uuidMayWithoutDashes; - else return REGEX.matcher(uuidMayWithoutDashes).replaceAll("$1-$2-$3-$4-$5"); - } - - default String toUuidWithoutDashes(String uuidMayWithDashes) { - if (!uuidMayWithDashes.contains("-")) return uuidMayWithDashes; - else return uuidMayWithDashes.replace("-", ""); - } } 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 index 7cbcce338a3..988022b88fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/friend/FriendItem.java @@ -20,8 +20,10 @@ import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.util.gson.JsonSerializable; +import java.util.UUID; + @JsonSerializable public record FriendItem( - @SerializedName("profileId") String profileId, + @SerializedName("profileId") UUID profileId, @SerializedName("name") String name) { }