feat(feedback): implement feedbacks.

This commit is contained in:
huanghongxun
2021-10-03 20:32:40 +08:00
parent c2a87e2474
commit dfb2d3f2bf
13 changed files with 522 additions and 198 deletions

View File

@@ -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<String, String> query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString()));
if (session.getMethod() == Method.POST) {
Map<String, String> 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<String, String> 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"));
}

View File

@@ -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<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====

View File

@@ -0,0 +1,161 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<HMCLAccount> account = new SimpleObjectProperty<>();
public static HMCLAccount getAccount() {
return account.get();
}
public static ObjectProperty<HMCLAccount> accountProperty() {
return account;
}
public static void setAccount(HMCLAccount account) {
HMCLAccounts.account.set(account);
}
public static Task<Void> 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;
}
}

View File

@@ -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<? extends Paint> 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<? extends Paint> 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);
}
}

View File

@@ -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.
*/

View File

@@ -0,0 +1,58 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<DecoratorAnimatedPage> {
protected DecoratorAnimatedPageSkin(DecoratorAnimatedPage control) {
super(control);
BorderPane pane = new BorderPane();
pane.setLeft(control.left);
pane.setCenter(control.center);
getChildren().setAll(pane);
}
}
}

View File

@@ -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<HMCLAccount> account = new SimpleObjectProperty<>();
public class FeedbackPage extends VBox implements PageAware {
private final ObservableList<FeedbackResponse> 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)).<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
}).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
Map<String, String> 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.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.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<FeedbackResponse> 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<FeedbackType> 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<FeedbackType> 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
}
}

View File

@@ -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> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> 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

View File

@@ -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

View File

@@ -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=啟動器版本

View File

@@ -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=启动器版本

View File

@@ -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" } ] } }
*
* <p>
* Error response: { "Identity":"0", "XErr":2148916238, "Message":"",
* "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" }
*
* <p>
* 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()

View File

@@ -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();
}
}