From 651aedaa509dabd32a3c70eb1b2aad82e0f841f4 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sat, 23 Oct 2021 02:22:04 +0800 Subject: [PATCH] feat(microsoft): use device code to login. --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 18 ++++++++++--- .../hmcl/ui/account/CreateAccountPane.java | 27 ++++++++++++++----- .../ui/account/OAuthAccountLoginDialog.java | 21 ++++++++++----- .../jackhuang/hmcl/ui/construct/HintPane.java | 6 +++++ HMCL/src/main/resources/assets/css/root.css | 4 +++ .../java/org/jackhuang/hmcl/auth/OAuth.java | 8 +++--- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 70280b0e1..68a1eeeb1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -77,6 +77,7 @@ import java.lang.reflect.Method; import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.BooleanSupplier; @@ -700,7 +701,7 @@ public final class FXUtils { Controllers.showToast(i18n("message.copied")); } - public static TextFlow segmentToTextFlow(final String segment, Consumer hyperlinkAction) { + public static List parseSegment(String segment, Consumer hyperlinkAction) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); @@ -719,6 +720,10 @@ public final class FXUtils { JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent()); hyperlink.setOnAction(e -> hyperlinkAction.accept(href)); texts.add(hyperlink); + } else if ("b".equals(element.getTagName())) { + Text text = new Text(element.getTextContent()); + text.getStyleClass().add("bold"); + texts.add(text); } else if ("br".equals(element.getTagName())) { texts.add(new Text("\n")); } else { @@ -728,12 +733,17 @@ public final class FXUtils { texts.add(new Text(node.getTextContent())); } } - final TextFlow tf = new TextFlow(texts.toArray(new javafx.scene.Node[0])); - return tf; + return texts; } catch (SAXException | ParserConfigurationException | IOException e) { LOG.log(Level.WARNING, "Failed to parse xml", e); - return new TextFlow(new Text(segment)); + return Collections.singletonList(new Text(segment)); } } + public static TextFlow segmentToTextFlow(final String segment, Consumer hyperlinkAction) { + TextFlow tf = new TextFlow(); + tf.getChildren().setAll(parseSegment(segment, hyperlinkAction)); + return tf; + } + } 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 a797df957..119b9e851 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 @@ -23,7 +23,9 @@ import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -53,6 +55,7 @@ import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; @@ -88,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { private final Pane detailsContainer; private final BooleanProperty logging = new SimpleBooleanProperty(); + private final ObjectProperty deviceCode = new SimpleObjectProperty<>(); + private final WeakListenerHolder holder = new WeakListenerHolder(); private TaskExecutor loginTask; @@ -217,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { } logging.set(true); + deviceCode.set(null); loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData)) .whenComplete(Schedulers.javafx(), account -> { @@ -262,15 +268,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { if (factory == Accounts.FACTORY_MICROSOFT) { VBox vbox = new VBox(8); HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); - hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> - logging - ? i18n("account.methods.microsoft.manual") - : i18n("account.methods.microsoft.hint"))); - hintPane.setOnMouseClicked(e -> { - if (logging.get() && OAuthServer.lastlyOpenedURL != null) { - FXUtils.copyText(OAuthServer.lastlyOpenedURL); + FXUtils.onChangeAndOperate(deviceCode, deviceCode -> { + if (deviceCode != null) { + hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode())); + } else { + hintPane.setSegment(i18n("account.methods.microsoft.hint")); } }); + hintPane.setOnMouseClicked(e -> { + if (deviceCode.get() != null) { + FXUtils.copyText(deviceCode.get().getVerificationUri()); + } + }); + + holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> { + runInFX(() -> deviceCode.set(value)); + })); HBox box = new HBox(8); JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java index 5f7b710d9..033c86250 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java @@ -15,8 +15,10 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jackhuang.hmcl.ui.construct.DialogPane; +import org.jackhuang.hmcl.ui.construct.HintPane; +import org.jackhuang.hmcl.ui.construct.JFXHyperlink; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import java.util.function.Consumer; import java.util.logging.Level; @@ -43,10 +45,13 @@ public class OAuthAccountLoginDialog extends DialogPane { Label usernameLabel = new Label(account.getUsername()); HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); - hintPane.textProperty().bind(BindingMapping.of(deviceCode).map(deviceCode -> - deviceCode != null - ? i18n("account.methods.microsoft.manual", deviceCode.getUserCode()) - : i18n("account.methods.microsoft.hint"))); + FXUtils.onChangeAndOperate(deviceCode, deviceCode -> { + if (deviceCode != null) { + hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode())); + } else { + hintPane.setSegment(i18n("account.methods.microsoft.hint")); + } + }); hintPane.setOnMouseClicked(e -> { if (deviceCode.get() != null) { FXUtils.copyText(deviceCode.get().getVerificationUri()); @@ -70,7 +75,9 @@ public class OAuthAccountLoginDialog extends DialogPane { } private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) { - deviceCode.set(event); + FXUtils.runInFX(() -> { + deviceCode.set(event); + }); } @Override 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 0682ad4f2..a8442be07 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 @@ -27,6 +27,8 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; public class HintPane extends VBox { @@ -84,6 +86,10 @@ public class HintPane extends VBox { this.text.set(text); } + public void setSegment(String segment) { + flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction)); + } + public void setChildren(Node... children) { flow.getChildren().setAll(children); } diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 8d016ceed..6be01d4e1 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -95,6 +95,10 @@ -fx-pref-width: 200; } +.bold { + -fx-font-weight: bold; +} + .memory-label { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java index 58e7bbb33..9cda0beba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java @@ -98,8 +98,9 @@ public class OAuth { .form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope)) .ignoreHttpCode() .getJson(DeviceTokenResponse.class); + handleErrorResponse(deviceTokenResponse); - options.callback.grantDeviceCode(deviceTokenResponse.deviceCode, deviceTokenResponse.verificationURI); + options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI); // Microsoft OAuth Flow options.callback.openBrowser(deviceTokenResponse.verificationURI); @@ -112,7 +113,7 @@ public class OAuth { // We stop waiting if user does not respond our authentication request in 15 minutes. long estimatedTime = System.nanoTime() - startTime; - if (TimeUnit.MINUTES.convert(estimatedTime, TimeUnit.SECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) { + if (TimeUnit.SECONDS.convert(estimatedTime, TimeUnit.NANOSECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) { throw new NoSelectedCharacterException(); } @@ -121,6 +122,7 @@ public class OAuth { pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), pair("code", deviceTokenResponse.deviceCode), pair("client_id", options.callback.getClientId())) + .ignoreHttpCode() .getJson(TokenResponse.class); if ("authorization_pending".equals(tokenResponse.error)) { @@ -256,7 +258,7 @@ public class OAuth { } } - private static class DeviceTokenResponse { + private static class DeviceTokenResponse extends ErrorResponse { @SerializedName("user_code") public String userCode;