From eea072cea5a07e000ea0c641959f4f6df3adfebe Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 14 Feb 2026 20:38:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=BE=AE=E8=BD=AF=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E7=99=BB=E5=BD=95=E5=AF=B9=E8=AF=9D=E6=A1=86=20(#5531?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/build.gradle.kts | 1 + .../hmcl/ui/account/CreateAccountPane.java | 19 +- .../ui/account/MicrosoftAccountLoginPane.java | 376 +++++++++--------- .../jackhuang/hmcl/ui/construct/HintPane.java | 7 +- .../org/jackhuang/hmcl/util/QrCodeUtils.java | 48 +++ HMCL/src/main/resources/assets/css/root.css | 5 + .../resources/assets/lang/I18N.properties | 12 +- .../resources/assets/lang/I18N_ar.properties | 1 - .../resources/assets/lang/I18N_es.properties | 1 - .../resources/assets/lang/I18N_ja.properties | 1 - .../resources/assets/lang/I18N_lzh.properties | 1 - .../resources/assets/lang/I18N_ru.properties | 1 - .../resources/assets/lang/I18N_uk.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 12 +- .../assets/lang/I18N_zh_CN.properties | 12 +- .../org/jackhuang/hmcl/util/StringUtils.java | 9 + gradle/libs.versions.toml | 2 + 17 files changed, 267 insertions(+), 242 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/QrCodeUtils.java diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 156969060..96275f51b 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation(libs.twelvemonkeys.imageio.webp) implementation(libs.java.info) implementation(libs.monet.fx) + implementation(libs.nayuki.qrcodegen) if (launcherExe.isBlank()) { implementation(libs.hmclauncher) 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 484fe44af..583e40326 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 @@ -273,27 +273,16 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { btnAccept.disableProperty().unbind(); detailsContainer.getChildren().remove(detailsPane); lblErrorMessage.setText(""); - lblErrorMessage.setVisible(true); - actions.setVisible(true); - actions.setManaged(true); + setActions(lblErrorMessage, actions); } if (factory == Accounts.FACTORY_MICROSOFT) { - VBox vbox = new VBox(8); - detailsPane = vbox; - HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); - hintPane.setText(i18n("account.methods.microsoft.hint")); - vbox.getChildren().addAll(new MicrosoftAccountLoginPane(true)); - btnAccept.setOnAction(e -> { - fireEvent(new DialogCloseEvent()); - Controllers.dialog(new MicrosoftAccountLoginPane()); - }); - actions.setManaged(false); - actions.setVisible(false); - btnAccept.setDisable(false); + detailsPane = new MicrosoftAccountLoginPane(true); + setActions(); } else { detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire); btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not()); + setActions(lblErrorMessage, actions); } detailsContainer.getChildren().add(detailsPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/MicrosoftAccountLoginPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/MicrosoftAccountLoginPane.java index 9273d5a62..7cd2b7c8b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/MicrosoftAccountLoginPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/MicrosoftAccountLoginPane.java @@ -2,19 +2,19 @@ package org.jackhuang.hmcl.ui.account; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; -import javafx.beans.binding.Bindings; +import com.jfoenix.controls.JFXSpinner; +import io.nayuki.qrcodegen.QrCode; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.geometry.Insets; -import javafx.geometry.Orientation; +import javafx.css.PseudoClass; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Group; import javafx.scene.control.Label; -import javafx.scene.control.Separator; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import javafx.scene.shape.FillRule; import javafx.scene.shape.SVGPath; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; @@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.OAuth; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; -import org.jackhuang.hmcl.game.OAuthServer; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -33,13 +32,14 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.upgrade.IntegrityChecker; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.QrCodeUtils; +import org.jackhuang.hmcl.util.StringUtils; import java.util.concurrent.CancellationException; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware { @@ -49,17 +49,14 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog @SuppressWarnings("FieldCanBeLocal") private final WeakListenerHolder holder = new WeakListenerHolder(); - private final ObjectProperty deviceCode = new SimpleObjectProperty<>(); - private final ObjectProperty browserUrl = new SimpleObjectProperty<>(); - private final Label lblCode; + + private final ObjectProperty step = new SimpleObjectProperty<>(); private TaskExecutor browserTaskExecutor; private TaskExecutor deviceTaskExecutor; + private final JFXButton btnLogin; private final SpinnerPane loginButtonSpinner; - private final HBox authMethodsContentBox = new HBox(0); - private final HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR); - private HintPane unofficialHintPane; public MicrosoftAccountLoginPane() { this(false); @@ -75,230 +72,166 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog this.cancelCallback = onCancel; getStyleClass().add("microsoft-login-dialog"); - if (!bodyonly) { + if (bodyonly) { + this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true); + } else { Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft")); heading.getStyleClass().add("header-label"); setHeading(heading); - } else { - setStyle("-fx-padding: 0px 0px 0px 0px;"); } + this.setMaxWidth(650); + onEscPressed(this, this::onCancel); - JFXButton btnLogin = new JFXButton(i18n("account.login")); + btnLogin = new JFXButton(i18n("account.login")); btnLogin.getStyleClass().add("dialog-accept"); - btnLogin.setOnAction(e -> startLoginTasks()); loginButtonSpinner = new SpinnerPane(); loginButtonSpinner.getStyleClass().add("small-spinner-pane"); loginButtonSpinner.setContent(btnLogin); - lblCode = new Label(); - lblCode.getStyleClass().add("code-label"); - lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\""); - JFXButton btnCancel = new JFXButton(i18n("button.cancel")); btnCancel.getStyleClass().add("dialog-cancel"); btnCancel.setOnAction(e -> onCancel()); - HBox actions = new HBox(10, loginButtonSpinner, btnCancel); - actions.setAlignment(Pos.CENTER_RIGHT); - setActions(actions); + setActions(loginButtonSpinner, btnCancel); + holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> { + if (step.get() instanceof Step.StartAuthorizationCodeLogin) + step.set(new Step.WaitForOpenBrowser(event.getUrl())); + })); + + holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> { + if (step.get() instanceof Step.StartDeviceCodeLogin) + step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri())); + })); + + this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty() + ? new Step.Init() + : new Step.StartAuthorizationCodeLogin()); + FXUtils.onChangeAndOperate(step, this::onStep); + } + + private void onStep(Step currentStep) { VBox rootContainer = new VBox(10); setBody(rootContainer); - rootContainer.setPadding(new Insets(5, 0, 0, 0)); rootContainer.setAlignment(Pos.TOP_CENTER); - HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); - hintPane.setText(i18n("account.methods.microsoft.hint")); - FXUtils.onChangeAndOperate(deviceCode, event -> { - if (event != null) - hintPane.setSegment(i18n("account.methods.microsoft.manual", event.getVerificationUri())); - }); - - errHintPane.managedProperty().bind(errHintPane.visibleProperty()); - errHintPane.setVisible(false); - rootContainer.getChildren().add(errHintPane); - if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) { - HintPane snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING); + var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING); snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot")); rootContainer.getChildren().add(snapshotHint); btnLogin.setDisable(true); + loginButtonSpinner.setLoading(false); return; } - rootContainer.getChildren().add(hintPane); - if (!IntegrityChecker.isOfficial()) { - unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING); - unofficialHintPane.managedProperty().bind(unofficialHintPane.visibleProperty()); + var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING); unofficialHintPane.setSegment(i18n("unofficial.hint")); rootContainer.getChildren().add(unofficialHintPane); } - initAuthMethodsBox(); - rootContainer.getChildren().add(authMethodsContentBox); + if (currentStep instanceof Step.Init) { + btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin())); + loginButtonSpinner.setLoading(false); - HBox linkBox = new HBox(15); - linkBox.setAlignment(Pos.CENTER); - linkBox.setPadding(new Insets(5, 0, 0, 0)); + var hintPane = new HintPane(MessageDialogPane.MessageType.INFO); + hintPane.setText(i18n("account.methods.microsoft.hint")); + rootContainer.getChildren().add(hintPane); + } else if (currentStep instanceof Step.StartAuthorizationCodeLogin) { + loginButtonSpinner.setLoading(true); + cancelAllTasks(); + + rootContainer.getChildren().add(new JFXSpinner()); + + browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE)) + .whenComplete(Schedulers.javafx(), this::onLoginCompleted) + .executor(true); + } else if (currentStep instanceof Step.StartDeviceCodeLogin) { + loginButtonSpinner.setLoading(true); + cancelAllTasks(); + + rootContainer.getChildren().add(new JFXSpinner()); + + deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE)) + .whenComplete(Schedulers.javafx(), this::onLoginCompleted) + .executor(true); + } else if (currentStep instanceof Step.WaitForOpenBrowser wait) { + btnLogin.setOnAction(e -> { + FXUtils.openLink(wait.url()); + loginButtonSpinner.setLoading(true); + }); + loginButtonSpinner.setLoading(false); + + HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); + hintPane.setSegment( + i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()), + FXUtils::copyText + ); + + rootContainer.getChildren().add(hintPane); + } else if (currentStep instanceof Step.WaitForScanQrCode wait) { + loginButtonSpinner.setLoading(true); + + var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO); + deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint", + StringUtils.escapeXmlAttribute(wait.verificationUri()), + wait.verificationUri(), + wait.userCode() + )); + + var qrCode = new SVGPath(); + qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary()); + qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(wait.verificationUri(), QrCode.Ecc.MEDIUM))); + qrCode.setScaleX(3); + qrCode.setScaleY(3); + + var lblCode = new Label(wait.userCode()); + lblCode.getStyleClass().add("code-label"); + lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";"); + + var codeBox = new StackPane(lblCode); + codeBox.getStyleClass().add("code-box"); + codeBox.setCursor(Cursor.HAND); + FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode())); + codeBox.setMaxWidth(USE_PREF_SIZE); + + rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox); + } else if (currentStep instanceof Step.LoginFailed failed) { + btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin())); + loginButtonSpinner.setLoading(false); + cancelAllTasks(); + + HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR); + errHintPane.setText(failed.message()); + rootContainer.getChildren().add(errHintPane); + } + + var linkBox = new FlowPane(8, 8); + linkBox.setAlignment(Pos.CENTER_LEFT); + linkBox.setPrefWrapLength(500); + + if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) { + JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device")); + useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin())); + linkBox.getChildren().add(useQrCode); + } else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) { + JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser")); + userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin())); + linkBox.getChildren().add(userBrowser); + } JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile")); profileLink.setExternalLink("https://account.live.com/editprof.aspx"); JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase")); purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL); - JFXHyperlink forgotLink = new JFXHyperlink(i18n("account.methods.forgot_password")); - forgotLink.setExternalLink("https://account.live.com/ResetPassword.aspx"); - linkBox.getChildren().addAll(profileLink, purchaseLink, forgotLink); + linkBox.getChildren().addAll(profileLink, purchaseLink); rootContainer.getChildren().add(linkBox); - FXUtils.onChangeAndOperate(deviceCode, event -> { - if (event != null) { - authMethodsContentBox.setVisible(true); - lblCode.setText(event.getUserCode()); - } - }); - - holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> runInFX(() -> deviceCode.set(value)))); - holder.add(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode.registerWeak(event -> runInFX(() -> browserUrl.set(event.getUrl())))); - } - - private void initAuthMethodsBox() { - authMethodsContentBox.managedProperty().bind(authMethodsContentBox.visibleProperty()); - authMethodsContentBox.setAlignment(Pos.CENTER); - authMethodsContentBox.setVisible(false); - authMethodsContentBox.setPrefWidth(600); - - VBox browserPanel = new VBox(10); - browserPanel.setAlignment(Pos.CENTER); - browserPanel.setPadding(new Insets(10)); - browserPanel.setPrefWidth(280); - HBox.setHgrow(browserPanel, Priority.ALWAYS); - - Label browserTitle = new Label(i18n("account.methods.microsoft.methods.browser")); - browserTitle.getStyleClass().add("method-title"); - - Label browserDesc = new Label(i18n("account.methods.microsoft.methods.browser.hint")); - browserDesc.getStyleClass().add("method-desc"); - - JFXButton btnOpenBrowser = FXUtils.newBorderButton(i18n("account.methods.microsoft.methods.browser.copy_open")); - btnOpenBrowser.setOnAction(e -> { - FXUtils.copyText(browserUrl.get()); - FXUtils.openLink(browserUrl.get()); - }); - btnOpenBrowser.disableProperty().bind(browserUrl.isNull()); - - browserPanel.getChildren().addAll(browserTitle, browserDesc, btnOpenBrowser); - - VBox separatorBox = new VBox(); - separatorBox.setAlignment(Pos.CENTER); - separatorBox.setMinWidth(30); - HBox.setHgrow(separatorBox, Priority.NEVER); - - Separator sepTop = new Separator(Orientation.VERTICAL); - VBox.setVgrow(sepTop, Priority.ALWAYS); - - Label orLabel = new Label(i18n("account.methods.microsoft.methods.or")); - orLabel.setPadding(new Insets(5, 0, 5, 0)); - orLabel.setStyle("-fx-text-fill: -monet-outline; -fx-font-size: 11px; -fx-font-weight: bold;"); - - Separator sepBottom = new Separator(Orientation.VERTICAL); - VBox.setVgrow(sepBottom, Priority.ALWAYS); - - separatorBox.getChildren().addAll(sepTop, orLabel, sepBottom); - - VBox devicePanel = new VBox(10); - devicePanel.setAlignment(Pos.CENTER); - devicePanel.setPadding(new Insets(10)); - devicePanel.setPrefWidth(280); - HBox.setHgrow(devicePanel, Priority.ALWAYS); - - Label deviceTitle = new Label(i18n("account.methods.microsoft.methods.device")); - deviceTitle.getStyleClass().add("method-title"); - - Label deviceDesc = new Label(); - deviceDesc.getStyleClass().add("method-desc"); - deviceDesc.textProperty().bind(Bindings.createStringBinding( - () -> i18n("account.methods.microsoft.methods.device.hint", deviceCode.get() == null ? "..." : deviceCode.get().getVerificationUri()), - deviceCode)); - - var qrCode = new SVGPath(); - qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary()); - qrCode.setFillRule(FillRule.EVEN_ODD); - qrCode.setContent("M740 740ZM0 0ZM240 80h20v20h-20zm80 0h20v20h-20zm120 0h20v20h-20zm20 0h20v20h-20zm-180 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-200 20h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm-240 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm-200 20h20v20h-20zm100 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm-200 20h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm-240 20h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm-240 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zM80 240h20v20H80zm80 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm-540 20h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm80 0h20v20h-20zm200 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm-480 20h20v20h-20zm80 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zM80 300h20v20H80zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zM80 320h20v20H80zm100 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zM80 340h20v20H80zm60 0h20v20h-20zm80 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm80 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zM80 360h20v20H80zm20 0h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm140 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zM80 380h20v20H80zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm100 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm120 0h20v20h-20zM80 400h20v20H80zm60 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm120 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zM80 420h20v20H80zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm240 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm-440 20h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-460 20h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm140 0h20v20h-20zM80 480h20v20H80zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-320 20h20v20h-20zm40 0h20v20h-20zm60 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-360 20h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm100 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm-380 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm120 0h20v20h-20zm80 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm-380 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm100 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm-360 20h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm80 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-360 20h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm60 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-380 20h20v20h-20zm60 0h20v20h-20zm140 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm-360 20h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm20 0h20v20h-20zm20 0h20v20h-20zm40 0h20v20h-20zm40 0h20v20h-20zm60 0h20v20h-20z M80 80h140v140H80Zm20 20h100v100H100Zm420-20h140v140H520Zm20 20h100v100H540Z M120 120h60v60h-60zm440 0h60v60h-60z M80 520h140v140H80Zm20 20h100v100H100Z M120 560h60v60h-60z"); - qrCode.setScaleX(80.0 / 740.0); - qrCode.setScaleY(80.0 / 740.0); - - HBox codeBox = new HBox(10); - codeBox.getStyleClass().add("code-box"); - - FXUtils.setLimitWidth(codeBox, 170); - FXUtils.setLimitHeight(codeBox, 40); - - codeBox.getChildren().add(lblCode); - devicePanel.getChildren().addAll(deviceTitle, deviceDesc, new Group(qrCode), codeBox); - - authMethodsContentBox.getChildren().addAll(browserPanel, separatorBox, devicePanel); - } - - private void startLoginTasks() { - deviceCode.set(null); - browserUrl.set(null); - errHintPane.setVisible(false); - - if (unofficialHintPane != null) { - unofficialHintPane.setVisible(false); - } - - loginButtonSpinner.showSpinner(); - - Task.FinalizedCallbackWithResult onComplete = (account, exception) -> { - if (exception == null) { - cancelAllTasks(); - runInFX(() -> onLoginCompleted(account)); - } else if (!(exception instanceof CancellationException)) { - errHintPane.setText(Accounts.localizeErrorMessage(exception)); - errHintPane.setVisible(true); - authMethodsContentBox.setVisible(false); - } - }; - - browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE)) - .whenComplete(Schedulers.javafx(), onComplete) - .executor(true); - - deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE)) - .whenComplete(Schedulers.javafx(), onComplete) - .executor(true); - } - - private void onLoginCompleted(MicrosoftAccount account) { - if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin); - - int oldIndex = Accounts.getAccounts().indexOf(account); - if (oldIndex == -1) { - Accounts.getAccounts().add(account); - } else { - Accounts.getAccounts().remove(oldIndex); - Accounts.getAccounts().add(oldIndex, account); - } - - Accounts.setSelectedAccount(account); - - if (loginCallback != null) { - try { - loginCallback.accept(account.logIn()); - } catch (AuthenticationException e) { - errHintPane.setText(Accounts.localizeErrorMessage(e)); - errHintPane.setVisible(true); - loginButtonSpinner.showSpinner(); - return; - } - } - fireEvent(new DialogCloseEvent()); + setBody(rootContainer); } private void cancelAllTasks() { @@ -311,5 +244,56 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog if (cancelCallback != null) cancelCallback.run(); fireEvent(new DialogCloseEvent()); } + + private void onLoginCompleted(MicrosoftAccount account, Exception exception) { + if (exception == null) { + if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin); + + int oldIndex = Accounts.getAccounts().indexOf(account); + if (oldIndex == -1) { + Accounts.getAccounts().add(account); + } else { + Accounts.getAccounts().remove(oldIndex); + Accounts.getAccounts().add(oldIndex, account); + } + + Accounts.setSelectedAccount(account); + + if (loginCallback != null) { + try { + loginCallback.accept(account.logIn()); + } catch (AuthenticationException e) { + this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e))); + return; + } + } + fireEvent(new DialogCloseEvent()); + } else if (!(exception instanceof CancellationException)) { + this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception))); + } + } + + private sealed interface Step { + final class Init implements Step { + } + + final class StartAuthorizationCodeLogin implements Step { + } + + record WaitForOpenBrowser(String url) implements Step { + + } + + final class StartDeviceCodeLogin implements Step { + } + + record WaitForScanQrCode(String userCode, String verificationUri) implements Step { + + } + + record LoginFailed(String message) implements Step { + } + } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/HintPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/HintPane.java index cfa318a3f..410e15c20 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/HintPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/HintPane.java @@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import java.util.Locale; +import java.util.function.Consumer; public class HintPane extends VBox { private final Text label = new Text(); @@ -66,7 +67,11 @@ public class HintPane extends VBox { } public void setSegment(String segment) { - flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction)); + this.setSegment(segment, Controllers::onHyperlinkAction); + } + + public void setSegment(String segment, Consumer hyperlinkAction) { + flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction)); } public void setChildren(Node... children) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/QrCodeUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/QrCodeUtils.java new file mode 100644 index 000000000..9174334c5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/QrCodeUtils.java @@ -0,0 +1,48 @@ +/* + * 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.util; + +import io.nayuki.qrcodegen.QrCode; + +/// @author Glavo +public final class QrCodeUtils { + public static String toSVGPath(QrCode qr) { + return toSVGPath(qr, 0); + } + + public static String toSVGPath(QrCode qr, int border) { + int actualSize = qr.size + border * 2; + + var builder = new StringBuilder(qr.size * qr.size * 12); + builder.append('M').append(actualSize).append(' ').append(actualSize).append("ZM0 0Z"); + + for (int y = 0; y < qr.size; y++) { + for (int x = 0; x < qr.size; x++) { + if (qr.getModule(x, y)) { + if (x != 0 || y != 0) + builder.append(' '); + builder.append("M").append(x + border).append(',').append(y + border).append("h1v1h-1z"); + } + } + } + return builder.toString(); + } + + private QrCodeUtils() { + } +} diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 31ae771a9..b22fc69b5 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -578,6 +578,10 @@ * * ******************************************************************************/ +.microsoft-login-dialog:bodyonly { + -fx-padding: 0px 0px 0px 0px; +} + .method-title { -fx-text-fill: -monet-on-surface; -fx-font-weight: bold; @@ -594,6 +598,7 @@ .code-box { -fx-background-color: -monet-surface-variant; -fx-background-radius: 6; + -fx-padding: 0 10 0 10; -fx-alignment: center; } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d1fff3f7e..ca9ea6d26 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -118,13 +118,10 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try account.methods.microsoft.logging_in=Logging in... account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account. -account.methods.microsoft.methods.or=Or -account.methods.microsoft.methods.device=Log in on another device -account.methods.microsoft.methods.device.hint=Scan the QR code on another device, or visit \n %s \n to complete log in -account.methods.microsoft.methods.device.copy=Copy code -account.methods.microsoft.methods.browser=Log in via browser (Recommended) -account.methods.microsoft.methods.browser.copy_open=Copy link and visit in browser -account.methods.microsoft.methods.browser.hint=Click the following button to copy and open the browser to log in +account.methods.microsoft.methods.device=Log In with QR Code +account.methods.microsoft.methods.device.hint=Scan QR code or visit %s to complete login, enter %s in the opened page. +account.methods.microsoft.methods.browser=Log In via Browser +account.methods.microsoft.methods.browser.hint=Click the "Log in" button or copy the link and paste it into the browser to log in. account.methods.microsoft.manual=If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection. account.methods.microsoft.profile=Account Profile account.methods.microsoft.purchase=Buy Minecraft @@ -144,7 +141,6 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play \n\ This option is for advanced users only. We do not recommend changing this option unless you know what you are doing. account.methods.offline.uuid.malformed=Invalid format. -account.methods.forgot_password=Forgot Password account.methods.ban_query=Ban Query account.missing=No Accounts account.missing.add=Click here to add one. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index b927dd8d6..d0fe89098 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID هو معرف فريد للاعبي Minec \n\ هذا الخيار للمستخدمين المتقدمين فقط. لا نوصي بتغيير هذا الخيار ما لم تعرف ما تفعله. account.methods.offline.uuid.malformed=تنسيق غير صالح. -account.methods.forgot_password=نسيت كلمة المرور account.methods.ban_query=استعلام الحظر account.missing=لا توجد حسابات account.missing.add=انقر هنا لإضافة واحد. diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 8100b987b..0538f73b6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -123,7 +123,6 @@ account.methods.offline.uuid.hint=UUID es un identificador único para los jugad \n\ Esta opción es sólo para usuarios avanzados. No recomendamos editar esta opción a menos que sepas lo que estás haciendo. account.methods.offline.uuid.malformed=Formato no válido. -account.methods.forgot_password=Olvidé mi contraseña account.methods.ban_query=Consulta de bloqueo de cuentas account.missing=No hay cuentas account.missing.add=Haga clic aquí para añadir una. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index f80ed6d82..f954d71e5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -91,7 +91,6 @@ account.methods.microsoft.birth=誕生日の設定を編集する方法... account.methods.microsoft.deauthorize=アカウントのバインドを解除 account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。 account.methods.microsoft.logging_in=ログイン... -account.methods.forgot_password=パスワードをお忘れの方 account.methods.microsoft.makegameidsettings=プロファイルを作成/プロフィール名を編集する account.methods.microsoft.profile=アカウントプロファイル.. account.methods.microsoft.purchase=Minecraftを購入する diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 759efba8f..dc2579160 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -110,7 +110,6 @@ account.methods.microsoft.logging_in=方登入…… account.methods.microsoft.makegameidsettings=添檔 / 書檔之名 account.methods.microsoft.profile=纂戶簿訊息 account.methods.microsoft.purchase=買礦藝 -account.methods.forgot_password=亡符節 account.methods.ban_query=檢戶簿羈否 account.methods.microsoft.snapshot.website=官網 account.methods.offline=離綫之式 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index c4d979c9a..2156c65a5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -122,7 +122,6 @@ account.methods.offline.name.invalid=Для имени пользователя account.methods.offline.uuid=UUID account.methods.offline.uuid.hint=UUID - это уникальный идентификатор игрового персонажа в Minecraft. Способ генерации UUID различается в разных лаунчерах игр. Изменение UUID на тот, который генерируется другим лаунчером гарантирует, что игровые блоки/предметы в рюкзаке вашего офлайн аккаунта останутся. Этот параметр предназначен для экспертов. Если вы не знаете что делаете, мы не советуем вам изменять этот параметр. Эта опция не требуется для присоединения к серверам. account.methods.offline.uuid.malformed=Недопустимый формат. -account.methods.forgot_password=Забыли пароль account.methods.ban_query=Просмотреть аккаунт заблокирован account.missing=Нет аккаунтов account.missing.add=Нажмите здесь, чтобы добавить. diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index b6b5c960f..a5024eda7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -120,7 +120,6 @@ account.methods.offline.uuid.hint=UUID - це унікальний іденти \n\ Ця опція призначена лише для досвідчених користувачів. Ми не рекомендуємо змінювати цю опцію, якщо ви не знаєте, що робите. account.methods.offline.uuid.malformed=Недійсний формат. -account.methods.forgot_password=Забули пароль account.methods.ban_query=Запит блокування account.missing=Немає облікових записів account.missing.add=Натисніть тут, щоб додати. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5e5b45d58..163ccac5f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -117,17 +117,13 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros account.methods.microsoft.logging_in=登入中…… account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱 account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。 -account.methods.microsoft.methods.or=或 -account.methods.microsoft.methods.device=在其他裝置登入 -account.methods.microsoft.methods.device.hint=在其他裝置掃描 QR Code,或開啟 \n %s \n 完成登入 -account.methods.microsoft.methods.device.copy=複製代碼 -account.methods.microsoft.methods.browser=在瀏覽器登入 (推薦) -account.methods.microsoft.methods.browser.copy_open=複製連結並在瀏覽器中開啟 -account.methods.microsoft.methods.browser.hint=點擊下方按鈕複製連結並開啟瀏覽器以登入 +account.methods.microsoft.methods.device=掃描 QR Code 登入 +account.methods.microsoft.methods.device.hint=掃描 QR Code 或訪問 %s,在開啟的頁面中輸入 %s 完成登入。 +account.methods.microsoft.methods.browser=在瀏覽器中登入 +account.methods.microsoft.methods.browser.hint=點擊「登入」按鈕或者複製連結並在瀏覽器中貼上以登入。 account.methods.microsoft.manual=若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。 account.methods.microsoft.profile=編輯帳戶個人資訊 account.methods.microsoft.purchase=購買 Minecraft -account.methods.forgot_password=忘記密碼 account.methods.ban_query=查詢帳戶是否被封禁 account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載 官方版本 進行登入。 account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 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 adc9a5db9..9eb2c2a5d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -118,18 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软 account.methods.microsoft.logging_in=登录中…… account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称 account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。 -account.methods.microsoft.methods.or=或 -account.methods.microsoft.methods.device=在其他设备登录 -account.methods.microsoft.methods.device.hint=在其他设备扫描二维码,或打开 \n %s \n 完成登录 -account.methods.microsoft.methods.device.copy=复制代码 -account.methods.microsoft.methods.browser=在浏览器登录 (推荐) -account.methods.microsoft.methods.browser.copy_open=复制链接并打开浏览器 -account.methods.microsoft.methods.browser.hint=点击下方按钮复制链接并打开浏览器登录 +account.methods.microsoft.methods.device=扫描二维码登录 +account.methods.microsoft.methods.device.hint=扫描二维码或访问 %s,在打开的页面中输入 %s 完成登录。 +account.methods.microsoft.methods.browser=在浏览器登录 +account.methods.microsoft.methods.browser.hint=点击“登录”按钮或者复制链接并在浏览器中粘贴以登录。 account.methods.microsoft.manual=若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。\n\ 如遇到问题,你可以点击右上角帮助按钮进行求助。 account.methods.microsoft.profile=编辑账户个人信息 account.methods.microsoft.purchase=购买 Minecraft -account.methods.forgot_password=忘记密码 account.methods.ban_query=检测账户是否被封禁 account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载 官方版本 来登录微软账户。 account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。请下载官方版本来刷新账户。 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 8ec4d9914..d39ede1f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -545,6 +545,15 @@ public final class StringUtils { return builder.toString(); } + public static String escapeXmlAttribute(String str) { + return str + .replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">") + .replace("'", "'"); + } + public static String repeats(char ch, int repeat) { StringBuilder result = new StringBuilder(); for (int i = 0; i < repeat; i++) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22acb3170..bbafe5cfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ java-info = "1.0" authlib-injector = "1.2.7" monet-fx = "0.4.0" terracotta = "0.4.2" +nayuki-qrcodegen = "1.8.0" # testing junit = "6.0.1" @@ -49,6 +50,7 @@ pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" } java-info = { module = "org.glavo:java-info", version.ref = "java-info" } authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" } monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" } +nayuki-qrcodegen = { module = "io.nayuki:qrcodegen", version.ref = "nayuki-qrcodegen" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }