feat(feedback): implement feedbacks.
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ====
|
||||
|
||||
161
HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java
Normal file
161
HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=啟動器版本
|
||||
|
||||
|
||||
@@ -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=启动器版本
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user