From dfb2d3f2bf00b69395f932e8512982f5e73001c0 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sun, 3 Oct 2021 20:32:40 +0800 Subject: [PATCH] feat(feedback): implement feedbacks. --- .../game/MicrosoftAuthenticationServer.java | 32 +- .../org/jackhuang/hmcl/setting/Accounts.java | 4 +- .../jackhuang/hmcl/setting/HMCLAccounts.java | 161 +++++++++ .../main/java/org/jackhuang/hmcl/ui/SVG.java | 12 + .../hmcl/ui/construct/TabHeader.java | 16 + .../ui/decorator/DecoratorAnimatedPage.java | 58 ++++ .../jackhuang/hmcl/ui/main/FeedbackPage.java | 321 ++++++++++-------- .../hmcl/ui/main/LauncherSettingsPage.java | 27 +- .../resources/assets/lang/I18N.properties | 10 +- .../resources/assets/lang/I18N_zh.properties | 12 +- .../assets/lang/I18N_zh_CN.properties | 12 +- .../hmcl/auth/microsoft/MicrosoftService.java | 42 +-- .../jackhuang/hmcl/util/io/HttpRequest.java | 13 + 13 files changed, 522 insertions(+), 198 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimatedPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java index a28fa5163..e8cb16abd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/MicrosoftAuthenticationServer.java @@ -29,6 +29,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -44,6 +45,8 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi public static String lastlyOpenedURL; + private String idToken; + private MicrosoftAuthenticationServer(int port) { super(port); @@ -60,15 +63,40 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi return future.get(); } + @Override + public String getIdToken() { + return idToken; + } + @Override public Response serve(IHTTPSession session) { - if (session.getMethod() != Method.GET || !"/auth-response".equals(session.getUri())) { + if (!"/auth-response".equals(session.getUri())) { return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, ""); } - Map query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString())); + + if (session.getMethod() == Method.POST) { + Map files = new HashMap<>(); + try { + session.parseBody(files); + } catch (IOException e) { + Logging.LOG.log(Level.WARNING, "Failed to read post data", e); + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, ""); + } catch (ResponseException re) { + return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); + } + } else if (session.getMethod() == Method.GET) { + // do nothing + } else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, ""); + } + String parameters = session.getQueryParameterString(); + + Map query = mapOf(NetworkUtils.parseQuery(parameters)); if (query.containsKey("code")) { + idToken = query.get("id_token"); future.complete(query.get("code")); } else { + Logging.LOG.warning("Error: " + parameters); future.completeExceptionally(new AuthenticationException("failed to authenticate")); } 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 daf196536..e8638e767 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -81,10 +81,12 @@ public final class Accounts { } } + public static final MicrosoftService.OAuthCallback MICROSOFT_OAUTH_CALLBACK = new MicrosoftAuthenticationServer.Factory(); + public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER); public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG; public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer); - public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(new MicrosoftAuthenticationServer.Factory())); + public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MICROSOFT_OAUTH_CALLBACK)); public static final List> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR); // ==== login type / account factory mapping ==== diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java new file mode 100644 index 000000000..b13c137fd --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java @@ -0,0 +1,161 @@ +/* + * 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.setting; + +import com.google.gson.annotations.SerializedName; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftService; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.io.NetworkUtils; + +import java.util.UUID; + +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; + +public class HMCLAccounts { + + private static final ObjectProperty account = new SimpleObjectProperty<>(); + + public static HMCLAccount getAccount() { + return account.get(); + } + + public static ObjectProperty accountProperty() { + return account; + } + + public static void setAccount(HMCLAccount account) { + HMCLAccounts.account.set(account); + } + + public static Task login() { + String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID()); + String scope = "openid offline_access"; + + return Task.supplyAsync(() -> { + MicrosoftService.OAuthSession session = Accounts.MICROSOFT_OAUTH_CALLBACK.startServer(); + Accounts.MICROSOFT_OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + mapOf( + pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()), + pair("response_type", "id_token code"), + pair("response_mode", "form_post"), + pair("scope", scope), + pair("redirect_uri", session.getRedirectURI()), + pair("nonce", nonce) + ))); + String code = session.waitFor(); + + // Authorization Code -> Token + String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token") + .form(mapOf(pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()), pair("code", code), + pair("grant_type", "authorization_code"), pair("client_secret", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientSecret()), + pair("redirect_uri", session.getRedirectURI()), pair("scope", scope))) + .getString(); + MicrosoftService.LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText, + MicrosoftService.LiveAuthorizationResponse.class); + + HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user") + .header("Token-Type", response.tokenType) + .header("Access-Token", response.accessToken) + .header("Authorization-Provider", "microsoft") + .authorization("Bearer", session.getIdToken()) + .getJson(HMCLAccountProfile.class); + + return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken); + }).thenAcceptAsync(Schedulers.javafx(), account -> { + setAccount(account); + }); + } + + public static class HMCLAccount implements HttpRequest.Authorization { + private final String provider; + private final String nickname; + private final String email; + private final String role; + private final String idToken; + private final String tokenType; + private final String accessToken; + private final String refreshToken; + + public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) { + this.provider = provider; + this.nickname = nickname; + this.email = email; + this.role = role; + this.idToken = idToken; + this.tokenType = tokenType; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public String getProvider() { + return provider; + } + + public String getNickname() { + return nickname; + } + + public String getEmail() { + return email; + } + + public String getRole() { + return role; + } + + public String getIdToken() { + return idToken; + } + + @Override + public String getTokenType() { + return tokenType; + } + + @Override + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + } + + private static class HMCLAccountProfile { + @SerializedName("ID") + String id; + @SerializedName("Provider") + String provider; + @SerializedName("Email") + String email; + @SerializedName("NickName") + String nickname; + @SerializedName("Role") + String role; + } + +} 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 56b87b7fb..47dfab74f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -514,4 +514,16 @@ public final class SVG { "M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z", fill, width, height); } + + public static Node thumbUpOutline(ObjectBinding fill, double width, double height) { + return createSVGPath( + "M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z", + fill, width, height); + } + + public static Node thumbDownOutline(ObjectBinding fill, double width, double height) { + return createSVGPath( + "M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z", + fill, width, height); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java index fcc1c763f..777bbe050 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java @@ -70,6 +70,22 @@ public class TabHeader extends Control implements TabControl { this.selectionModel.set(selectionModel); } + public void select(Tab tab) { + Tab oldTab = getSelectionModel().getSelectedItem(); + if (oldTab != null) { + if (oldTab.getNode() instanceof PageAware) { + ((PageAware) oldTab.getNode()).onPageHidden(); + } + } + + tab.initializeIfNeeded(); + if (tab.getNode() instanceof PageAware) { + ((PageAware) tab.getNode()).onPageShown(); + } + + getSelectionModel().select(tab); + } + /** * The position to place the tabs. */ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimatedPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimatedPage.java new file mode 100644 index 000000000..d0c20d82d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimatedPage.java @@ -0,0 +1,58 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.decorator; + +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +public class DecoratorAnimatedPage extends Control { + + private final VBox left = new VBox(); + private final StackPane center = new StackPane(); + + protected void setLeft(Node... children) { + left.getChildren().setAll(children); + } + + protected void setCenter(Node... children) { + center.getChildren().setAll(children); + } + + @Override + protected Skin createDefaultSkin() { + return new DecoratorAnimatedPageSkin(this); + } + + private static class DecoratorAnimatedPageSkin extends SkinBase { + + protected DecoratorAnimatedPageSkin(DecoratorAnimatedPage control) { + super(control); + + BorderPane pane = new BorderPane(); + pane.setLeft(control.left); + pane.setCenter(control.center); + getChildren().setAll(pane); + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java index 9dd8ef914..706c742d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java @@ -18,17 +18,21 @@ package org.jackhuang.hmcl.ui.main; import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.jfoenix.controls.*; import javafx.beans.binding.Bindings; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.*; +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer; +import org.jackhuang.hmcl.setting.HMCLAccounts; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -36,13 +40,17 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.io.IOException; import java.net.HttpURLConnection; import java.util.List; +import java.util.Locale; +import java.util.Map; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; @@ -50,8 +58,7 @@ import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class FeedbackPage extends VBox { - private final ObjectProperty account = new SimpleObjectProperty<>(); +public class FeedbackPage extends VBox implements PageAware { private final ObservableList feedbacks = FXCollections.observableArrayList(); private final SpinnerPane spinnerPane = new SpinnerPane(); @@ -66,11 +73,14 @@ public class FeedbackPage extends VBox { TwoLineListItem accountInfo = new TwoLineListItem(); HBox.setHgrow(accountInfo, Priority.ALWAYS); - accountInfo.titleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname())); - accountInfo.subtitleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail())); + accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) + .map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname())); + accountInfo.subtitleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) + .map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail())); JFXButton logButton = new JFXButton(); - logButton.textProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.login") : i18n("account.logout"))); + logButton.textProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) + .map(account -> account == null ? i18n("account.login") : i18n("account.logout"))); logButton.setOnAction(e -> log()); loginPane.getChildren().setAll(accountInfo, logButton); @@ -83,16 +93,21 @@ public class FeedbackPage extends VBox { getChildren().add(searchPane); JFXTextField searchField = new JFXTextField(); - searchField.setOnAction(e -> search(searchField.getText())); + searchField.setOnAction(e -> search(searchField.getText(), "time", true)); HBox.setHgrow(searchField, Priority.ALWAYS); searchField.setPromptText(i18n("search")); JFXButton searchButton = new JFXButton(); searchButton.getStyleClass().add("toggle-icon4"); searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1)); - searchButton.setOnAction(e -> addFeedback()); + searchButton.setOnAction(e -> search(searchField.getText(), "time", true)); - searchPane.getChildren().setAll(searchField, searchButton); + JFXButton addButton = new JFXButton(); + addButton.getStyleClass().add("toggle-icon4"); + addButton.setGraphic(SVG.plus(Theme.blackFillBinding(), -1, -1)); + addButton.setOnAction(e -> addFeedback()); + + searchPane.getChildren().setAll(searchField, searchButton, addButton); } { @@ -115,10 +130,10 @@ public class FeedbackPage extends VBox { setSelectable(); likeButton.getStyleClass().add("toggle-icon4"); - likeButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); + likeButton.setGraphic(FXUtils.limitingSize(SVG.thumbUpOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); unlikeButton.getStyleClass().add("toggle-icon4"); - unlikeButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); + unlikeButton.setGraphic(FXUtils.limitingSize(SVG.thumbDownOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); container.getChildren().setAll(content, likeButton, unlikeButton); @@ -128,31 +143,63 @@ public class FeedbackPage extends VBox { @Override protected void updateControl(FeedbackResponse feedback, boolean empty) { + if (empty) return; content.setTitle(feedback.getTitle()); - content.setSubtitle(feedback.getContent()); + content.setSubtitle(feedback.getAuthor()); content.getTags().add("#" + feedback.getId()); - content.getTags().add(feedback.getAuthor()); - content.getTags().add(feedback.getLauncherVersion()); - content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase())); + content.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US))); + content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US))); } }); listView.setOnMouseClicked(e -> { if (listView.getSelectionModel().getSelectedIndex() < 0) return; FeedbackResponse selectedItem = listView.getSelectionModel().getSelectedItem(); - Controllers.dialog(new ViewFeedbackDialog(selectedItem)); + getFeedback(selectedItem.getId()) + .thenAcceptAsync(Schedulers.javafx(), f -> { + Controllers.dialog(new ViewFeedbackDialog(f)); + }) + .start(); }); getChildren().add(spinnerPane); } } - private void search(String keyword) { + @Override + public void onPageShown() { + search("", "time", false); + } + + private void search(String keyword, String order, boolean showAll) { + HMCLAccounts.HMCLAccount account = HMCLAccounts.getAccount(); Task.supplyAsync(() -> { - return HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback", pair("s", keyword)).>getJson(new TypeToken>(){}.getType()); - }).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> { + Map query = mapOf( + pair("keyword", keyword), + pair("order", order) + ); + if (showAll) { + query.put("showAll", "1"); + } + HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query)); + if (account != null) { + req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) + .header("Authorization-Provider", HMCLAccounts.getAccount().getProvider()); + } + return req.>getJson(new TypeToken>(){}.getType()); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { spinnerPane.hideSpinner(); if (exception != null) { + if (exception instanceof ResponseCodeException) { + int responseCode = ((ResponseCodeException) exception).getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + spinnerPane.setFailedReason(i18n("feedback.failed.permission")); + return; + } else if (responseCode == 429) { + spinnerPane.setFailedReason(i18n("feedback.failed.too_frequently")); + return; + } + } spinnerPane.setFailedReason(i18n("feedback.failed")); } else { feedbacks.setAll(result); @@ -160,18 +207,22 @@ public class FeedbackPage extends VBox { }).start(); } + private Task getFeedback(int id) { + return Task.supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback/" + id).getJson(FeedbackResponse.class)); + } + private void log() { - if (account.get() == null) { + if (HMCLAccounts.getAccount() == null) { // login Controllers.dialog(new LoginDialog()); } else { // logout - account.set(null); + HMCLAccounts.setAccount(null); } } private void addFeedback() { - if (account.get() == null) { + if (HMCLAccounts.getAccount() == null) { Controllers.dialog(i18n("feedback.add.login")); return; } @@ -179,143 +230,71 @@ public class FeedbackPage extends VBox { Controllers.dialog(new AddFeedbackDialog()); } - private static class HMCLAccount { - private final String nickname; - private final String email; - private final String accessToken; - - public HMCLAccount(String nickname, String email, String accessToken) { - this.nickname = nickname; - this.email = email; - this.accessToken = accessToken; - } - - public String getNickname() { - return nickname; - } - - public String getEmail() { - return email; - } - - public String getAccessToken() { - return accessToken; - } - } - - private static class HMCLLoginResponse { - private final int err; - private final String nickname; - private final String email; - private final String accessToken; - - public HMCLLoginResponse(int err, String nickname, String email, String accessToken) { - this.err = err; - this.nickname = nickname; - this.email = email; - this.accessToken = accessToken; - } - - public int getErr() { - return err; - } - - public String getNickname() { - return nickname; - } - - public String getEmail() { - return email; - } - - public String getAccessToken() { - return accessToken; - } - - public static final int ERR_OK = 0; - public static final int ERR_WRONG = 400; - } - private class LoginDialog extends JFXDialogLayout { private final SpinnerPane spinnerPane = new SpinnerPane(); private final Label errorLabel = new Label(); + private final BooleanProperty logging = new SimpleBooleanProperty(); public LoginDialog() { setHeading(new Label(i18n("feedback.login"))); - GridPane body = new GridPane(); - ColumnConstraints fieldColumn = new ColumnConstraints(); - fieldColumn.setFillWidth(true); - body.getColumnConstraints().setAll(new ColumnConstraints(), fieldColumn); - body.setVgap(8); - body.setHgap(8); - setBody(body); - - JFXTextField usernameField = new JFXTextField(); - usernameField.setValidators(new RequiredValidator()); - body.addRow(0, new Label(i18n("account.username")), usernameField); - - JFXPasswordField passwordField = new JFXPasswordField(); - passwordField.setValidators(new RequiredValidator()); - body.addRow(1, new Label(i18n("account.password")), passwordField); - - JFXButton registerButton = new JFXButton(); - registerButton.setText(i18n("account.register")); - registerButton.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/user/login")); + VBox vbox = new VBox(8); + setBody(vbox); + 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() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) { + FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL); + } + }); + vbox.getChildren().setAll(hintPane); JFXButton loginButton = new JFXButton(); spinnerPane.setContent(loginButton); loginButton.setText(i18n("account.login")); - loginButton.setOnAction(e -> login(usernameField.getText(), passwordField.getText())); + loginButton.setOnAction(e -> login()); JFXButton cancelButton = new JFXButton(); cancelButton.setText(i18n("button.cancel")); cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); onEscPressed(this, cancelButton::fire); - setActions(errorLabel, registerButton, spinnerPane, cancelButton); + setActions(errorLabel, spinnerPane, cancelButton); } - private void login(String username, String password) { + private void login() { spinnerPane.showSpinner(); errorLabel.setText(""); - Task.supplyAsync(() -> { - return HttpRequest.POST("https://hmcl.huangyuhui.net/api/user/login") - .json(mapOf( - pair("username", username), - pair("password", password) - )).getJson(HMCLLoginResponse.class); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + logging.set(true); + + HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> { + logging.set(false); if (exception != null) { if (exception instanceof IOException) { - if (exception instanceof ResponseCodeException && ((ResponseCodeException) exception).getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) { - errorLabel.setText(i18n("account.failed.invalid_password")); - } else { - errorLabel.setText(i18n("account.failed.connect_authentication_server")); - } + errorLabel.setText(i18n("account.failed.connect_authentication_server")); } else if (exception instanceof JsonParseException) { errorLabel.setText(i18n("account.failed.server_response_malformed")); } else { errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage()); } } else { - if (result.err == HMCLLoginResponse.ERR_OK) { - account.setValue(new HMCLAccount(result.getNickname(), result.getEmail(), result.getAccessToken())); - fireEvent(new DialogCloseEvent()); - } else if (result.err == HMCLLoginResponse.ERR_WRONG) { - errorLabel.setText(i18n("account.failed.invalid_password")); - } else { - errorLabel.setText(i18n("account.failed", result.err)); - } + fireEvent(new DialogCloseEvent()); } }).start(); } } - private static class AddFeedbackDialog extends JFXDialogLayout { + private static class AddFeedbackDialog extends DialogPane { + + JFXTextField titleField = new JFXTextField(); + JFXComboBox comboBox = new JFXComboBox<>(); + JFXTextArea contentArea = new JFXTextArea(); public AddFeedbackDialog() { - setHeading(new Label(i18n("feedback.add"))); + setTitle(i18n("feedback.add")); GridPane body = new GridPane(); body.setVgap(8); @@ -331,11 +310,9 @@ public class FeedbackPage extends VBox { titleHintPane.setText(i18n("feedback.add.hint.title")); body.addRow(1, titleHintPane); - JFXTextField titleField = new JFXTextField(); titleField.setValidators(new RequiredValidator()); body.addRow(2, new Label(i18n("feedback.title")), titleField); - JFXComboBox comboBox = new JFXComboBox<>(); comboBox.setMaxWidth(-1); comboBox.getItems().setAll(FeedbackType.values()); comboBox.getSelectionModel().select(0); @@ -346,29 +323,46 @@ public class FeedbackPage extends VBox { GridPane.setColumnSpan(contentLabel, 2); body.addRow(4, contentLabel); - JFXTextArea contentArea = new JFXTextArea(); contentArea.setValidators(new RequiredValidator()); contentArea.setPromptText(i18n("feedback.add.hint.content")); GridPane.setColumnSpan(contentArea, 2); body.addRow(5, contentArea); + validProperty().bind(Bindings.createBooleanBinding(() -> { + return titleField.validate() && contentArea.validate(); + }, titleField.textProperty(), contentArea.textProperty())); + setBody(body); - - JFXButton okButton = new JFXButton(); - okButton.setText(i18n("button.ok")); - okButton.setOnAction(e -> addFeedback(titleField.getText(), comboBox.getSelectionModel().getSelectedItem(), contentArea.getText())); - - JFXButton cancelButton = new JFXButton(); - cancelButton.setText(i18n("button.cancel")); - cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - onEscPressed(this, cancelButton::fire); - - setActions(okButton, cancelButton); } - private void addFeedback(String title, FeedbackType feedbackType, String content) { - fireEvent(new DialogCloseEvent()); - // TODO + @Override + protected void onAccept() { + setLoading(); + + addFeedback(titleField.getText(), comboBox.getValue(), contentArea.getText()) + .whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + onFailure(exception.getLocalizedMessage()); + } else { + onSuccess(); + } + }) + .start(); + } + + private Task addFeedback(String title, FeedbackType feedbackType, String content) { + return Task.runAsync(() -> { + HttpRequest.POST("https://hmcl.huangyuhui.net/api/feedback") + .json(mapOf( + pair("title", title), + pair("content", content), + pair("type", feedbackType.name().toLowerCase(Locale.ROOT)), + pair("launcher_version", Metadata.VERSION) + )) + .authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) + .header("Authorization-Provider", HMCLAccounts.getAccount().getProvider()) + .getString(); + }); } } @@ -377,9 +371,11 @@ public class FeedbackPage extends VBox { public ViewFeedbackDialog(FeedbackResponse feedback) { BorderPane heading = new BorderPane(); TwoLineListItem left = new TwoLineListItem(); + heading.setLeft(left); left.setTitle(feedback.getTitle()); left.setSubtitle(feedback.getAuthor()); left.getTags().add("#" + feedback.getId()); + left.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US))); left.getTags().add(feedback.getLauncherVersion()); left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase())); @@ -387,7 +383,17 @@ public class FeedbackPage extends VBox { Label content = new Label(feedback.getContent()); content.setWrapText(true); - setBody(content); + + TwoLineListItem response = new TwoLineListItem(); + response.getStyleClass().setAll("two-line-item-second-large"); + response.setTitle(i18n("feedback.response")); + response.setSubtitle(StringUtils.isBlank(feedback.getReason()) + ? i18n("feedback.response.empty") + : feedback.getReason()); + + VBox body = new VBox(content, response); + body.setSpacing(8); + setBody(body); JFXButton okButton = new JFXButton(); okButton.setText(i18n("button.ok")); @@ -402,18 +408,21 @@ public class FeedbackPage extends VBox { private final String title; private final String content; private final String author; + @SerializedName("launcher_version") private final String launcherVersion; - private final String gameVersion; private final FeedbackType type; + private final FeedbackState state; + private final String reason; - public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, String gameVersion, FeedbackType type) { + public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, FeedbackType type, FeedbackState state, String reason) { this.id = id; this.title = title; this.content = content; this.author = author; this.launcherVersion = launcherVersion; - this.gameVersion = gameVersion; this.type = type; + this.state = state; + this.reason = reason; } public int getId() { @@ -436,17 +445,27 @@ public class FeedbackPage extends VBox { return launcherVersion; } - public String getGameVersion() { - return gameVersion; - } - public FeedbackType getType() { return type; } + + public FeedbackState getState() { + return state; + } + + public String getReason() { + return reason; + } } private enum FeedbackType { - FEATURE_REQUEST, - BUG_REPORT + FEATURE, + BUG + } + + private enum FeedbackState { + OPEN, + REJECTED, + ACCEPTED } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java index 1f635b0c9..2901af308 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.FXUtils; @@ -28,13 +27,14 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations; 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 org.jackhuang.hmcl.ui.versions.VersionSettingsPage; import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class LauncherSettingsPage extends BorderPane implements DecoratorPage { +public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1)); private final TabHeader tab; private final TabHeader.Tab gameTab = new TabHeader.Tab<>("versionSettingsPage"); @@ -58,11 +58,10 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage { aboutTab.setNodeSupplier(AboutPage::new); tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab); - tab.getSelectionModel().select(gameTab); + tab.select(gameTab); gameTab.initializeIfNeeded(); gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null); FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> { - newValue.initializeIfNeeded(); transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer()); }); @@ -72,51 +71,51 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage { settingsItem.setTitle(i18n("settings.type.global.manage")); settingsItem.setLeftGraphic(wrap(SVG::gamepad)); settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab)); - settingsItem.setOnAction(e -> tab.getSelectionModel().select(gameTab)); + settingsItem.setOnAction(e -> tab.select(gameTab)); }) .startCategory(i18n("launcher")) .addNavigationDrawerItem(settingsItem -> { settingsItem.setTitle(i18n("settings.launcher.general")); settingsItem.setLeftGraphic(wrap(SVG::applicationOutline)); settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab)); - settingsItem.setOnAction(e -> tab.getSelectionModel().select(settingsTab)); + settingsItem.setOnAction(e -> tab.select(settingsTab)); }) .addNavigationDrawerItem(personalizationItem -> { personalizationItem.setTitle(i18n("settings.launcher.appearance")); personalizationItem.setLeftGraphic(wrap(SVG::styleOutline)); personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab)); - personalizationItem.setOnAction(e -> tab.getSelectionModel().select(personalizationTab)); + personalizationItem.setOnAction(e -> tab.select(personalizationTab)); }) .addNavigationDrawerItem(downloadItem -> { downloadItem.setTitle(i18n("download")); downloadItem.setLeftGraphic(wrap(SVG::downloadOutline)); downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab)); - downloadItem.setOnAction(e -> tab.getSelectionModel().select(downloadTab)); + downloadItem.setOnAction(e -> tab.select(downloadTab)); }) .startCategory(i18n("help")) .addNavigationDrawerItem(helpItem -> { helpItem.setTitle(i18n("help")); helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline)); helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab)); - helpItem.setOnAction(e -> tab.getSelectionModel().select(helpTab)); + helpItem.setOnAction(e -> tab.select(helpTab)); }) .addNavigationDrawerItem(feedbackItem -> { feedbackItem.setTitle(i18n("feedback")); feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline)); feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab)); - feedbackItem.setOnAction(e -> tab.getSelectionModel().select(feedbackTab)); + feedbackItem.setOnAction(e -> tab.select(feedbackTab)); }) .addNavigationDrawerItem(sponsorItem -> { sponsorItem.setTitle(i18n("sponsor")); sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline)); sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab)); - sponsorItem.setOnAction(e -> tab.getSelectionModel().select(sponsorTab)); + sponsorItem.setOnAction(e -> tab.select(sponsorTab)); }) .addNavigationDrawerItem(aboutItem -> { aboutItem.setTitle(i18n("about")); aboutItem.setLeftGraphic(wrap(SVG::informationOutline)); aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab)); - aboutItem.setOnAction(e -> tab.getSelectionModel().select(aboutTab)); + aboutItem.setOnAction(e -> tab.select(aboutTab)); }); FXUtils.setLimitWidth(sideBar, 200); setLeft(sideBar); @@ -127,11 +126,11 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage { public void showGameSettings(Profile profile) { gameTab.getNode().loadVersion(profile, null); - tab.getSelectionModel().select(gameTab); + tab.select(gameTab); } public void showFeedback() { - tab.getSelectionModel().select(feedbackTab); + tab.select(feedbackTab); } @Override diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b64e9a9c6..9b987afc5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -297,17 +297,21 @@ feedback.add.login=You must login/register HMCL account to gain feedback permiss feedback.add.permission=You must gain feedback permission to add new feedback. feedback.author=Author feedback.content=Content +feedback.empty=No items that meet the conditions. feedback.failed=Failed to load +feedback.failed.permission=You can only search with keywords when logged in an account with permission to submit feedbacks. +feedback.failed.too_frequently=Too frequently. Try again later. feedback.like=Like feedback.login=Log in to HMCL account feedback.response=Response +feedback.response.empty=Not responded feedback.state.accepted=Accepted -feedback.state.pending=Pending +feedback.state.open=Pending feedback.state.rejected=Rejected feedback.title=Title feedback.type=Type -feedback.type.bug_report=Bug Report -feedback.type.feature_request=Feature Request +feedback.type.bug=Bug Report +feedback.type.feature=Feature Request feedback.unlike=Unlike feedback.version=Launcher Version diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index ba8008979..c06bbe6d0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -290,24 +290,28 @@ fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自 feedback=回饋 feedback.add=新增回饋 -feedback.add.hint.search_before_add=添加回饋前,請先搜尋已有回饋中是否已經有人提出過相關內容,如果有,你可以透過給對應回饋按讚來提升對應回饋的優先度。 +feedback.add.hint.search_before_add=添加回饋前,請先搜尋已有回饋中是否已經有人提出過相關內容,如果有,你可以透過給對應回饋按讚來提升對應回饋的優先度。\n請你發布有意義的回饋。如果您發布了違反國家法律法規、灌水等不良訊息,或求助遊戲內容玩法等問題,你的帳號將被封禁。 feedback.add.hint.title=回饋標題需能簡練概括你的需求。"我有問題"、"我有一個想法"、"遊戲打不開" 等無法讓其他人一眼看出大致問題的標題是不被接受的。 feedback.add.hint.content=回饋內容需完整且簡練地表達你的需求。如果你遇到了問題,你需要詳細描述復現路徑,比如在打開啟動器後通過點擊什麼按鈕,做了什麼操作後觸發了什麼問題。如果你希望添加新功能,你需要闡述:為什麼玩家需要該功能,該功能能解決什麼問題,該功能可以怎麼實現。 feedback.add.login=你需要先登入/註冊 HMCL 回饋帳號並獲得回饋權限才能添加回饋。 feedback.add.permission=你需要獲得回饋權限才能添加回饋。 feedback.author=發布者 feedback.content=正文 +feedback.empty=沒有滿足條件的回饋 feedback.failed=載入失敗 +feedback.failed.permission=登錄帳戶並取得回饋權限後才能根據關鍵字搜索回饋 +feedback.failed.too_frequently=搜索次數太頻繁啦 feedback.like=贊成 feedback.login=登入 HMCL 帳號 feedback.response=回復 +feedback.response.empty=尚未查看 feedback.state.accepted=接受 -feedback.state.pending=審核中 +feedback.state.open=開放 feedback.state.rejected=拒絕 feedback.title=標題 feedback.type=類型 -feedback.type.bug_report=問題回饋 -feedback.type.feature_request=新功能請求 +feedback.type.bug=問題回饋 +feedback.type.feature=新功能請求 feedback.unlike=反對 feedback.version=啟動器版本 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 90a839ba2..225627a39 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -290,24 +290,28 @@ fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自 feedback=反馈 feedback.add=新增反馈 -feedback.add.hint.search_before_add=添加反馈前,请先搜索已有反馈中是否已经有人提出过相关内容,如果有,你可以通过给对应反馈点赞来提升对应反馈的优先级。 +feedback.add.hint.search_before_add=添加反馈前,请先搜索已有反馈中是否已经有人提出过相关内容,如果有,你可以通过给对应反馈点赞来提升对应反馈的优先级。\n请你发布有意义的反馈。如果您发布了违反国家法律法规、灌水等不良信息,或求助游戏内容玩法等问题,你的账号将被封禁。 feedback.add.hint.title=反馈标题需能简练概括你的需求。带有 "我有问题"、"我有一个想法"、"游戏打不开" 等无法让其他人一眼看出大致问题的标题的反馈将会被直接关闭。 feedback.add.hint.content=反馈内容需完整且简练地表达你的需求。如果你遇到了问题,你需要详细描述复现路径,比如在打开启动器后通过点击什么按钮,做了什么操作后触发了什么问题。如果你希望添加新功能,你需要阐述:为什么玩家需要该功能,该功能能解决什么问题,该功能可以怎么实现。 feedback.add.login=你需要先登录/注册 HMCL 反馈帐户并获得反馈权限才能添加反馈。 feedback.add.permission=你需要获得反馈权限才能添加反馈。 feedback.author=发布者 feedback.content=正文 +feedback.empty=没有满足条件的反馈 feedback.failed=加载失败 +feedback.failed.permission=登录账户并取得反馈权限后才能根据关键字搜索反馈 +feedback.failed.too_frequently=搜索次数太频繁啦 feedback.like=赞成 feedback.login=登录 HMCL 帐户 feedback.response=回复 +feedback.response.empty=尚未查看 feedback.state.accepted=接受 -feedback.state.pending=审核中 +feedback.state.open=开放 feedback.state.rejected=拒绝 feedback.title=标题 feedback.type=类型 -feedback.type.bug_report=问题反馈 -feedback.type.feature_request=新功能请求 +feedback.type.bug=问题反馈 +feedback.type.feature=新功能请求 feedback.unlike=反对 feedback.version=启动器版本 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 7e168e31e..42af0c334 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 @@ -53,7 +53,7 @@ public class MicrosoftService { private static final String AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf"; private static final String ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf"; private static final String SCOPE = "XboxLive.signin offline_access"; - private static final int[] PORTS = { 29111, 29112, 29113, 29114, 29115 }; + private static final int[] PORTS = {29111, 29112, 29113, 29114, 29115}; private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS); private static final Pattern OAUTH_URL_PATTERN = Pattern @@ -257,19 +257,20 @@ public class MicrosoftService { private static void getXBoxProfile(String uhs, String xstsToken) throws IOException { HttpRequest.GET("https://profile.xboxlive.com/users/me/profile/settings", - pair("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," - + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," - + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," - + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined")) + pair("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," + + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," + + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined")) .accept("application/json") - .authorization(String.format("XBL3.0 x=%s;%s", uhs, xstsToken)).header("x-xbl-contract-version", "3") + .authorization(String.format("XBL3.0 x=%s;%s", uhs, xstsToken)) + .header("x-xbl-contract-version", "3") .getString(); } private static MinecraftProfileResponse getMinecraftProfile(String tokenType, String accessToken) throws IOException, AuthenticationException { HttpURLConnection conn = HttpRequest.GET("https://api.minecraftservices.com/minecraft/profile") - .authorization(String.format("%s %s", tokenType, accessToken)) + .authorization(tokenType, accessToken) .createConnection(); int responseCode = conn.getResponseCode(); if (responseCode == HTTP_NOT_FOUND) { @@ -341,27 +342,27 @@ public class MicrosoftService { * redirect URI used to obtain the authorization * code.","correlation_id":"??????"} */ - private static class LiveAuthorizationResponse { + public static class LiveAuthorizationResponse { @SerializedName("token_type") - String tokenType; + public String tokenType; @SerializedName("expires_in") - int expiresIn; + public int expiresIn; @SerializedName("scope") - String scope; + public String scope; @SerializedName("access_token") - String accessToken; + public String accessToken; @SerializedName("refresh_token") - String refreshToken; + public String refreshToken; @SerializedName("user_id") - String userId; + public String userId; @SerializedName("foci") - String foci; + public String foci; } private static class LiveRefreshResponse { @@ -391,14 +392,13 @@ public class MicrosoftService { } /** - * * Success Response: { "IssueInstant":"2020-12-07T19:52:08.4463796Z", * "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{ * "xui":[ { "uhs":"userhash" } ] } } - * + *

* Error response: { "Identity":"0", "XErr":2148916238, "Message":"", * "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" } - * + *

* XErr Candidates: 2148916233 = missing XBox account 2148916238 = child account * not linked to a family */ @@ -524,12 +524,16 @@ public class MicrosoftService { /** * Wait for authentication - * + * * @return authentication code * @throws InterruptedException if interrupted * @throws ExecutionException if an I/O error occurred. */ String waitFor() throws InterruptedException, ExecutionException; + + default String getIdToken() { + return null; + } } private static final Gson GSON = new GsonBuilder() 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 705f60f46..7b151b473 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 @@ -63,6 +63,14 @@ public abstract class HttpRequest { return header("Authorization", token); } + public HttpRequest authorization(String tokenType, String tokenString) { + return authorization(tokenType + " " + tokenString); + } + + public HttpRequest authorization(Authorization authorization) { + return authorization(authorization.getTokenType(), authorization.getAccessToken()); + } + public HttpRequest header(String key, String value) { headers.put(key, value); return this; @@ -189,4 +197,9 @@ public abstract class HttpRequest { return new HttpPostRequest(url); } + public interface Authorization { + String getTokenType(); + + String getAccessToken(); + } }