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.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -44,6 +45,8 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
|||||||
|
|
||||||
public static String lastlyOpenedURL;
|
public static String lastlyOpenedURL;
|
||||||
|
|
||||||
|
private String idToken;
|
||||||
|
|
||||||
private MicrosoftAuthenticationServer(int port) {
|
private MicrosoftAuthenticationServer(int port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
|
||||||
@@ -60,15 +63,40 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
|||||||
return future.get();
|
return future.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdToken() {
|
||||||
|
return idToken;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response serve(IHTTPSession session) {
|
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, "");
|
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")) {
|
if (query.containsKey("code")) {
|
||||||
|
idToken = query.get("id_token");
|
||||||
future.complete(query.get("code"));
|
future.complete(query.get("code"));
|
||||||
} else {
|
} else {
|
||||||
|
Logging.LOG.warning("Error: " + parameters);
|
||||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
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 OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
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 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);
|
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||||
|
|
||||||
// ==== login type / account factory mapping ====
|
// ==== 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",
|
"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);
|
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);
|
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.
|
* 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;
|
package org.jackhuang.hmcl.ui.main;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.jfoenix.controls.*;
|
import com.jfoenix.controls.*;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.*;
|
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.setting.Theme;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
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.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
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.HttpRequest;
|
||||||
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.util.List;
|
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.onEscPressed;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
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.Pair.pair;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class FeedbackPage extends VBox {
|
public class FeedbackPage extends VBox implements PageAware {
|
||||||
private final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
|
||||||
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
|
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
|
||||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
|
|
||||||
@@ -66,11 +73,14 @@ public class FeedbackPage extends VBox {
|
|||||||
|
|
||||||
TwoLineListItem accountInfo = new TwoLineListItem();
|
TwoLineListItem accountInfo = new TwoLineListItem();
|
||||||
HBox.setHgrow(accountInfo, Priority.ALWAYS);
|
HBox.setHgrow(accountInfo, Priority.ALWAYS);
|
||||||
accountInfo.titleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
|
accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
|
||||||
accountInfo.subtitleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
|
.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();
|
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());
|
logButton.setOnAction(e -> log());
|
||||||
|
|
||||||
loginPane.getChildren().setAll(accountInfo, logButton);
|
loginPane.getChildren().setAll(accountInfo, logButton);
|
||||||
@@ -83,16 +93,21 @@ public class FeedbackPage extends VBox {
|
|||||||
getChildren().add(searchPane);
|
getChildren().add(searchPane);
|
||||||
|
|
||||||
JFXTextField searchField = new JFXTextField();
|
JFXTextField searchField = new JFXTextField();
|
||||||
searchField.setOnAction(e -> search(searchField.getText()));
|
searchField.setOnAction(e -> search(searchField.getText(), "time", true));
|
||||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||||
searchField.setPromptText(i18n("search"));
|
searchField.setPromptText(i18n("search"));
|
||||||
|
|
||||||
JFXButton searchButton = new JFXButton();
|
JFXButton searchButton = new JFXButton();
|
||||||
searchButton.getStyleClass().add("toggle-icon4");
|
searchButton.getStyleClass().add("toggle-icon4");
|
||||||
searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1));
|
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();
|
setSelectable();
|
||||||
|
|
||||||
likeButton.getStyleClass().add("toggle-icon4");
|
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.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);
|
container.getChildren().setAll(content, likeButton, unlikeButton);
|
||||||
|
|
||||||
@@ -128,31 +143,63 @@ public class FeedbackPage extends VBox {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateControl(FeedbackResponse feedback, boolean empty) {
|
protected void updateControl(FeedbackResponse feedback, boolean empty) {
|
||||||
|
if (empty) return;
|
||||||
content.setTitle(feedback.getTitle());
|
content.setTitle(feedback.getTitle());
|
||||||
content.setSubtitle(feedback.getContent());
|
content.setSubtitle(feedback.getAuthor());
|
||||||
content.getTags().add("#" + feedback.getId());
|
content.getTags().add("#" + feedback.getId());
|
||||||
content.getTags().add(feedback.getAuthor());
|
content.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)));
|
||||||
content.getTags().add(feedback.getLauncherVersion());
|
content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
|
||||||
content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
listView.setOnMouseClicked(e -> {
|
listView.setOnMouseClicked(e -> {
|
||||||
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
||||||
return;
|
return;
|
||||||
FeedbackResponse selectedItem = listView.getSelectionModel().getSelectedItem();
|
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);
|
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(() -> {
|
Task.supplyAsync(() -> {
|
||||||
return HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback", pair("s", keyword)).<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
|
Map<String, String> query = mapOf(
|
||||||
}).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
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();
|
spinnerPane.hideSpinner();
|
||||||
if (exception != null) {
|
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"));
|
spinnerPane.setFailedReason(i18n("feedback.failed"));
|
||||||
} else {
|
} else {
|
||||||
feedbacks.setAll(result);
|
feedbacks.setAll(result);
|
||||||
@@ -160,18 +207,22 @@ public class FeedbackPage extends VBox {
|
|||||||
}).start();
|
}).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() {
|
private void log() {
|
||||||
if (account.get() == null) {
|
if (HMCLAccounts.getAccount() == null) {
|
||||||
// login
|
// login
|
||||||
Controllers.dialog(new LoginDialog());
|
Controllers.dialog(new LoginDialog());
|
||||||
} else {
|
} else {
|
||||||
// logout
|
// logout
|
||||||
account.set(null);
|
HMCLAccounts.setAccount(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFeedback() {
|
private void addFeedback() {
|
||||||
if (account.get() == null) {
|
if (HMCLAccounts.getAccount() == null) {
|
||||||
Controllers.dialog(i18n("feedback.add.login"));
|
Controllers.dialog(i18n("feedback.add.login"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,143 +230,71 @@ public class FeedbackPage extends VBox {
|
|||||||
Controllers.dialog(new AddFeedbackDialog());
|
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 class LoginDialog extends JFXDialogLayout {
|
||||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
private final Label errorLabel = new Label();
|
private final Label errorLabel = new Label();
|
||||||
|
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||||
|
|
||||||
public LoginDialog() {
|
public LoginDialog() {
|
||||||
setHeading(new Label(i18n("feedback.login")));
|
setHeading(new Label(i18n("feedback.login")));
|
||||||
|
|
||||||
GridPane body = new GridPane();
|
VBox vbox = new VBox(8);
|
||||||
ColumnConstraints fieldColumn = new ColumnConstraints();
|
setBody(vbox);
|
||||||
fieldColumn.setFillWidth(true);
|
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||||
body.getColumnConstraints().setAll(new ColumnConstraints(), fieldColumn);
|
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||||
body.setVgap(8);
|
logging
|
||||||
body.setHgap(8);
|
? i18n("account.methods.microsoft.manual")
|
||||||
setBody(body);
|
: i18n("account.methods.microsoft.hint")));
|
||||||
|
hintPane.setOnMouseClicked(e -> {
|
||||||
JFXTextField usernameField = new JFXTextField();
|
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||||
usernameField.setValidators(new RequiredValidator());
|
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||||
body.addRow(0, new Label(i18n("account.username")), usernameField);
|
}
|
||||||
|
});
|
||||||
JFXPasswordField passwordField = new JFXPasswordField();
|
vbox.getChildren().setAll(hintPane);
|
||||||
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"));
|
|
||||||
|
|
||||||
JFXButton loginButton = new JFXButton();
|
JFXButton loginButton = new JFXButton();
|
||||||
spinnerPane.setContent(loginButton);
|
spinnerPane.setContent(loginButton);
|
||||||
loginButton.setText(i18n("account.login"));
|
loginButton.setText(i18n("account.login"));
|
||||||
loginButton.setOnAction(e -> login(usernameField.getText(), passwordField.getText()));
|
loginButton.setOnAction(e -> login());
|
||||||
|
|
||||||
JFXButton cancelButton = new JFXButton();
|
JFXButton cancelButton = new JFXButton();
|
||||||
cancelButton.setText(i18n("button.cancel"));
|
cancelButton.setText(i18n("button.cancel"));
|
||||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||||
onEscPressed(this, cancelButton::fire);
|
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();
|
spinnerPane.showSpinner();
|
||||||
errorLabel.setText("");
|
errorLabel.setText("");
|
||||||
Task.supplyAsync(() -> {
|
logging.set(true);
|
||||||
return HttpRequest.POST("https://hmcl.huangyuhui.net/api/user/login")
|
|
||||||
.json(mapOf(
|
HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
pair("username", username),
|
logging.set(false);
|
||||||
pair("password", password)
|
|
||||||
)).getJson(HMCLLoginResponse.class);
|
|
||||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
if (exception instanceof IOException) {
|
if (exception instanceof IOException) {
|
||||||
if (exception instanceof ResponseCodeException && ((ResponseCodeException) exception).getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) {
|
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
|
||||||
errorLabel.setText(i18n("account.failed.invalid_password"));
|
|
||||||
} else {
|
|
||||||
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
|
|
||||||
}
|
|
||||||
} else if (exception instanceof JsonParseException) {
|
} else if (exception instanceof JsonParseException) {
|
||||||
errorLabel.setText(i18n("account.failed.server_response_malformed"));
|
errorLabel.setText(i18n("account.failed.server_response_malformed"));
|
||||||
} else {
|
} else {
|
||||||
errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage());
|
errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (result.err == HMCLLoginResponse.ERR_OK) {
|
fireEvent(new DialogCloseEvent());
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).start();
|
}).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() {
|
public AddFeedbackDialog() {
|
||||||
setHeading(new Label(i18n("feedback.add")));
|
setTitle(i18n("feedback.add"));
|
||||||
|
|
||||||
GridPane body = new GridPane();
|
GridPane body = new GridPane();
|
||||||
body.setVgap(8);
|
body.setVgap(8);
|
||||||
@@ -331,11 +310,9 @@ public class FeedbackPage extends VBox {
|
|||||||
titleHintPane.setText(i18n("feedback.add.hint.title"));
|
titleHintPane.setText(i18n("feedback.add.hint.title"));
|
||||||
body.addRow(1, titleHintPane);
|
body.addRow(1, titleHintPane);
|
||||||
|
|
||||||
JFXTextField titleField = new JFXTextField();
|
|
||||||
titleField.setValidators(new RequiredValidator());
|
titleField.setValidators(new RequiredValidator());
|
||||||
body.addRow(2, new Label(i18n("feedback.title")), titleField);
|
body.addRow(2, new Label(i18n("feedback.title")), titleField);
|
||||||
|
|
||||||
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
|
|
||||||
comboBox.setMaxWidth(-1);
|
comboBox.setMaxWidth(-1);
|
||||||
comboBox.getItems().setAll(FeedbackType.values());
|
comboBox.getItems().setAll(FeedbackType.values());
|
||||||
comboBox.getSelectionModel().select(0);
|
comboBox.getSelectionModel().select(0);
|
||||||
@@ -346,29 +323,46 @@ public class FeedbackPage extends VBox {
|
|||||||
GridPane.setColumnSpan(contentLabel, 2);
|
GridPane.setColumnSpan(contentLabel, 2);
|
||||||
body.addRow(4, contentLabel);
|
body.addRow(4, contentLabel);
|
||||||
|
|
||||||
JFXTextArea contentArea = new JFXTextArea();
|
|
||||||
contentArea.setValidators(new RequiredValidator());
|
contentArea.setValidators(new RequiredValidator());
|
||||||
contentArea.setPromptText(i18n("feedback.add.hint.content"));
|
contentArea.setPromptText(i18n("feedback.add.hint.content"));
|
||||||
GridPane.setColumnSpan(contentArea, 2);
|
GridPane.setColumnSpan(contentArea, 2);
|
||||||
body.addRow(5, contentArea);
|
body.addRow(5, contentArea);
|
||||||
|
|
||||||
|
validProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
return titleField.validate() && contentArea.validate();
|
||||||
|
}, titleField.textProperty(), contentArea.textProperty()));
|
||||||
|
|
||||||
setBody(body);
|
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) {
|
@Override
|
||||||
fireEvent(new DialogCloseEvent());
|
protected void onAccept() {
|
||||||
// TODO
|
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) {
|
public ViewFeedbackDialog(FeedbackResponse feedback) {
|
||||||
BorderPane heading = new BorderPane();
|
BorderPane heading = new BorderPane();
|
||||||
TwoLineListItem left = new TwoLineListItem();
|
TwoLineListItem left = new TwoLineListItem();
|
||||||
|
heading.setLeft(left);
|
||||||
left.setTitle(feedback.getTitle());
|
left.setTitle(feedback.getTitle());
|
||||||
left.setSubtitle(feedback.getAuthor());
|
left.setSubtitle(feedback.getAuthor());
|
||||||
left.getTags().add("#" + feedback.getId());
|
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(feedback.getLauncherVersion());
|
||||||
left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
|
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());
|
Label content = new Label(feedback.getContent());
|
||||||
content.setWrapText(true);
|
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();
|
JFXButton okButton = new JFXButton();
|
||||||
okButton.setText(i18n("button.ok"));
|
okButton.setText(i18n("button.ok"));
|
||||||
@@ -402,18 +408,21 @@ public class FeedbackPage extends VBox {
|
|||||||
private final String title;
|
private final String title;
|
||||||
private final String content;
|
private final String content;
|
||||||
private final String author;
|
private final String author;
|
||||||
|
@SerializedName("launcher_version")
|
||||||
private final String launcherVersion;
|
private final String launcherVersion;
|
||||||
private final String gameVersion;
|
|
||||||
private final FeedbackType type;
|
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.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.launcherVersion = launcherVersion;
|
this.launcherVersion = launcherVersion;
|
||||||
this.gameVersion = gameVersion;
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.state = state;
|
||||||
|
this.reason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getId() {
|
public int getId() {
|
||||||
@@ -436,17 +445,27 @@ public class FeedbackPage extends VBox {
|
|||||||
return launcherVersion;
|
return launcherVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGameVersion() {
|
|
||||||
return gameVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeedbackType getType() {
|
public FeedbackType getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FeedbackState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum FeedbackType {
|
private enum FeedbackType {
|
||||||
FEATURE_REQUEST,
|
FEATURE,
|
||||||
BUG_REPORT
|
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.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.setting.Profiles;
|
import org.jackhuang.hmcl.setting.Profiles;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
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.animation.TransitionPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
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.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
|
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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 ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
|
||||||
private final TabHeader tab;
|
private final TabHeader tab;
|
||||||
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
|
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);
|
aboutTab.setNodeSupplier(AboutPage::new);
|
||||||
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab);
|
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab);
|
||||||
|
|
||||||
tab.getSelectionModel().select(gameTab);
|
tab.select(gameTab);
|
||||||
gameTab.initializeIfNeeded();
|
gameTab.initializeIfNeeded();
|
||||||
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
|
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
|
||||||
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||||
newValue.initializeIfNeeded();
|
|
||||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
|
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.setTitle(i18n("settings.type.global.manage"));
|
||||||
settingsItem.setLeftGraphic(wrap(SVG::gamepad));
|
settingsItem.setLeftGraphic(wrap(SVG::gamepad));
|
||||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
|
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
|
||||||
settingsItem.setOnAction(e -> tab.getSelectionModel().select(gameTab));
|
settingsItem.setOnAction(e -> tab.select(gameTab));
|
||||||
})
|
})
|
||||||
.startCategory(i18n("launcher"))
|
.startCategory(i18n("launcher"))
|
||||||
.addNavigationDrawerItem(settingsItem -> {
|
.addNavigationDrawerItem(settingsItem -> {
|
||||||
settingsItem.setTitle(i18n("settings.launcher.general"));
|
settingsItem.setTitle(i18n("settings.launcher.general"));
|
||||||
settingsItem.setLeftGraphic(wrap(SVG::applicationOutline));
|
settingsItem.setLeftGraphic(wrap(SVG::applicationOutline));
|
||||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
|
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
|
||||||
settingsItem.setOnAction(e -> tab.getSelectionModel().select(settingsTab));
|
settingsItem.setOnAction(e -> tab.select(settingsTab));
|
||||||
})
|
})
|
||||||
.addNavigationDrawerItem(personalizationItem -> {
|
.addNavigationDrawerItem(personalizationItem -> {
|
||||||
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
|
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
|
||||||
personalizationItem.setLeftGraphic(wrap(SVG::styleOutline));
|
personalizationItem.setLeftGraphic(wrap(SVG::styleOutline));
|
||||||
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
|
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
|
||||||
personalizationItem.setOnAction(e -> tab.getSelectionModel().select(personalizationTab));
|
personalizationItem.setOnAction(e -> tab.select(personalizationTab));
|
||||||
})
|
})
|
||||||
.addNavigationDrawerItem(downloadItem -> {
|
.addNavigationDrawerItem(downloadItem -> {
|
||||||
downloadItem.setTitle(i18n("download"));
|
downloadItem.setTitle(i18n("download"));
|
||||||
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
||||||
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
|
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
|
||||||
downloadItem.setOnAction(e -> tab.getSelectionModel().select(downloadTab));
|
downloadItem.setOnAction(e -> tab.select(downloadTab));
|
||||||
})
|
})
|
||||||
.startCategory(i18n("help"))
|
.startCategory(i18n("help"))
|
||||||
.addNavigationDrawerItem(helpItem -> {
|
.addNavigationDrawerItem(helpItem -> {
|
||||||
helpItem.setTitle(i18n("help"));
|
helpItem.setTitle(i18n("help"));
|
||||||
helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||||
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
|
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
|
||||||
helpItem.setOnAction(e -> tab.getSelectionModel().select(helpTab));
|
helpItem.setOnAction(e -> tab.select(helpTab));
|
||||||
})
|
})
|
||||||
.addNavigationDrawerItem(feedbackItem -> {
|
.addNavigationDrawerItem(feedbackItem -> {
|
||||||
feedbackItem.setTitle(i18n("feedback"));
|
feedbackItem.setTitle(i18n("feedback"));
|
||||||
feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||||
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
|
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
|
||||||
feedbackItem.setOnAction(e -> tab.getSelectionModel().select(feedbackTab));
|
feedbackItem.setOnAction(e -> tab.select(feedbackTab));
|
||||||
})
|
})
|
||||||
.addNavigationDrawerItem(sponsorItem -> {
|
.addNavigationDrawerItem(sponsorItem -> {
|
||||||
sponsorItem.setTitle(i18n("sponsor"));
|
sponsorItem.setTitle(i18n("sponsor"));
|
||||||
sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline));
|
sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline));
|
||||||
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
|
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
|
||||||
sponsorItem.setOnAction(e -> tab.getSelectionModel().select(sponsorTab));
|
sponsorItem.setOnAction(e -> tab.select(sponsorTab));
|
||||||
})
|
})
|
||||||
.addNavigationDrawerItem(aboutItem -> {
|
.addNavigationDrawerItem(aboutItem -> {
|
||||||
aboutItem.setTitle(i18n("about"));
|
aboutItem.setTitle(i18n("about"));
|
||||||
aboutItem.setLeftGraphic(wrap(SVG::informationOutline));
|
aboutItem.setLeftGraphic(wrap(SVG::informationOutline));
|
||||||
aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab));
|
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);
|
FXUtils.setLimitWidth(sideBar, 200);
|
||||||
setLeft(sideBar);
|
setLeft(sideBar);
|
||||||
@@ -127,11 +126,11 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
|||||||
|
|
||||||
public void showGameSettings(Profile profile) {
|
public void showGameSettings(Profile profile) {
|
||||||
gameTab.getNode().loadVersion(profile, null);
|
gameTab.getNode().loadVersion(profile, null);
|
||||||
tab.getSelectionModel().select(gameTab);
|
tab.select(gameTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showFeedback() {
|
public void showFeedback() {
|
||||||
tab.getSelectionModel().select(feedbackTab);
|
tab.select(feedbackTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.add.permission=You must gain feedback permission to add new feedback.
|
||||||
feedback.author=Author
|
feedback.author=Author
|
||||||
feedback.content=Content
|
feedback.content=Content
|
||||||
|
feedback.empty=No items that meet the conditions.
|
||||||
feedback.failed=Failed to load
|
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.like=Like
|
||||||
feedback.login=Log in to HMCL account
|
feedback.login=Log in to HMCL account
|
||||||
feedback.response=Response
|
feedback.response=Response
|
||||||
|
feedback.response.empty=Not responded
|
||||||
feedback.state.accepted=Accepted
|
feedback.state.accepted=Accepted
|
||||||
feedback.state.pending=Pending
|
feedback.state.open=Pending
|
||||||
feedback.state.rejected=Rejected
|
feedback.state.rejected=Rejected
|
||||||
feedback.title=Title
|
feedback.title=Title
|
||||||
feedback.type=Type
|
feedback.type=Type
|
||||||
feedback.type.bug_report=Bug Report
|
feedback.type.bug=Bug Report
|
||||||
feedback.type.feature_request=Feature Request
|
feedback.type.feature=Feature Request
|
||||||
feedback.unlike=Unlike
|
feedback.unlike=Unlike
|
||||||
feedback.version=Launcher Version
|
feedback.version=Launcher Version
|
||||||
|
|
||||||
|
|||||||
@@ -290,24 +290,28 @@ fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自
|
|||||||
|
|
||||||
feedback=回饋
|
feedback=回饋
|
||||||
feedback.add=新增回饋
|
feedback.add=新增回饋
|
||||||
feedback.add.hint.search_before_add=添加回饋前,請先搜尋已有回饋中是否已經有人提出過相關內容,如果有,你可以透過給對應回饋按讚來提升對應回饋的優先度。
|
feedback.add.hint.search_before_add=添加回饋前,請先搜尋已有回饋中是否已經有人提出過相關內容,如果有,你可以透過給對應回饋按讚來提升對應回饋的優先度。\n請你發布有意義的回饋。如果您發布了違反國家法律法規、灌水等不良訊息,或求助遊戲內容玩法等問題,你的帳號將被封禁。
|
||||||
feedback.add.hint.title=回饋標題需能簡練概括你的需求。"我有問題"、"我有一個想法"、"遊戲打不開" 等無法讓其他人一眼看出大致問題的標題是不被接受的。
|
feedback.add.hint.title=回饋標題需能簡練概括你的需求。"我有問題"、"我有一個想法"、"遊戲打不開" 等無法讓其他人一眼看出大致問題的標題是不被接受的。
|
||||||
feedback.add.hint.content=回饋內容需完整且簡練地表達你的需求。如果你遇到了問題,你需要詳細描述復現路徑,比如在打開啟動器後通過點擊什麼按鈕,做了什麼操作後觸發了什麼問題。如果你希望添加新功能,你需要闡述:為什麼玩家需要該功能,該功能能解決什麼問題,該功能可以怎麼實現。
|
feedback.add.hint.content=回饋內容需完整且簡練地表達你的需求。如果你遇到了問題,你需要詳細描述復現路徑,比如在打開啟動器後通過點擊什麼按鈕,做了什麼操作後觸發了什麼問題。如果你希望添加新功能,你需要闡述:為什麼玩家需要該功能,該功能能解決什麼問題,該功能可以怎麼實現。
|
||||||
feedback.add.login=你需要先登入/註冊 HMCL 回饋帳號並獲得回饋權限才能添加回饋。
|
feedback.add.login=你需要先登入/註冊 HMCL 回饋帳號並獲得回饋權限才能添加回饋。
|
||||||
feedback.add.permission=你需要獲得回饋權限才能添加回饋。
|
feedback.add.permission=你需要獲得回饋權限才能添加回饋。
|
||||||
feedback.author=發布者
|
feedback.author=發布者
|
||||||
feedback.content=正文
|
feedback.content=正文
|
||||||
|
feedback.empty=沒有滿足條件的回饋
|
||||||
feedback.failed=載入失敗
|
feedback.failed=載入失敗
|
||||||
|
feedback.failed.permission=登錄帳戶並取得回饋權限後才能根據關鍵字搜索回饋
|
||||||
|
feedback.failed.too_frequently=搜索次數太頻繁啦
|
||||||
feedback.like=贊成
|
feedback.like=贊成
|
||||||
feedback.login=登入 HMCL 帳號
|
feedback.login=登入 HMCL 帳號
|
||||||
feedback.response=回復
|
feedback.response=回復
|
||||||
|
feedback.response.empty=尚未查看
|
||||||
feedback.state.accepted=接受
|
feedback.state.accepted=接受
|
||||||
feedback.state.pending=審核中
|
feedback.state.open=開放
|
||||||
feedback.state.rejected=拒絕
|
feedback.state.rejected=拒絕
|
||||||
feedback.title=標題
|
feedback.title=標題
|
||||||
feedback.type=類型
|
feedback.type=類型
|
||||||
feedback.type.bug_report=問題回饋
|
feedback.type.bug=問題回饋
|
||||||
feedback.type.feature_request=新功能請求
|
feedback.type.feature=新功能請求
|
||||||
feedback.unlike=反對
|
feedback.unlike=反對
|
||||||
feedback.version=啟動器版本
|
feedback.version=啟動器版本
|
||||||
|
|
||||||
|
|||||||
@@ -290,24 +290,28 @@ fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自
|
|||||||
|
|
||||||
feedback=反馈
|
feedback=反馈
|
||||||
feedback.add=新增反馈
|
feedback.add=新增反馈
|
||||||
feedback.add.hint.search_before_add=添加反馈前,请先搜索已有反馈中是否已经有人提出过相关内容,如果有,你可以通过给对应反馈点赞来提升对应反馈的优先级。
|
feedback.add.hint.search_before_add=添加反馈前,请先搜索已有反馈中是否已经有人提出过相关内容,如果有,你可以通过给对应反馈点赞来提升对应反馈的优先级。\n请你发布有意义的反馈。如果您发布了违反国家法律法规、灌水等不良信息,或求助游戏内容玩法等问题,你的账号将被封禁。
|
||||||
feedback.add.hint.title=反馈标题需能简练概括你的需求。带有 "我有问题"、"我有一个想法"、"游戏打不开" 等无法让其他人一眼看出大致问题的标题的反馈将会被直接关闭。
|
feedback.add.hint.title=反馈标题需能简练概括你的需求。带有 "我有问题"、"我有一个想法"、"游戏打不开" 等无法让其他人一眼看出大致问题的标题的反馈将会被直接关闭。
|
||||||
feedback.add.hint.content=反馈内容需完整且简练地表达你的需求。如果你遇到了问题,你需要详细描述复现路径,比如在打开启动器后通过点击什么按钮,做了什么操作后触发了什么问题。如果你希望添加新功能,你需要阐述:为什么玩家需要该功能,该功能能解决什么问题,该功能可以怎么实现。
|
feedback.add.hint.content=反馈内容需完整且简练地表达你的需求。如果你遇到了问题,你需要详细描述复现路径,比如在打开启动器后通过点击什么按钮,做了什么操作后触发了什么问题。如果你希望添加新功能,你需要阐述:为什么玩家需要该功能,该功能能解决什么问题,该功能可以怎么实现。
|
||||||
feedback.add.login=你需要先登录/注册 HMCL 反馈帐户并获得反馈权限才能添加反馈。
|
feedback.add.login=你需要先登录/注册 HMCL 反馈帐户并获得反馈权限才能添加反馈。
|
||||||
feedback.add.permission=你需要获得反馈权限才能添加反馈。
|
feedback.add.permission=你需要获得反馈权限才能添加反馈。
|
||||||
feedback.author=发布者
|
feedback.author=发布者
|
||||||
feedback.content=正文
|
feedback.content=正文
|
||||||
|
feedback.empty=没有满足条件的反馈
|
||||||
feedback.failed=加载失败
|
feedback.failed=加载失败
|
||||||
|
feedback.failed.permission=登录账户并取得反馈权限后才能根据关键字搜索反馈
|
||||||
|
feedback.failed.too_frequently=搜索次数太频繁啦
|
||||||
feedback.like=赞成
|
feedback.like=赞成
|
||||||
feedback.login=登录 HMCL 帐户
|
feedback.login=登录 HMCL 帐户
|
||||||
feedback.response=回复
|
feedback.response=回复
|
||||||
|
feedback.response.empty=尚未查看
|
||||||
feedback.state.accepted=接受
|
feedback.state.accepted=接受
|
||||||
feedback.state.pending=审核中
|
feedback.state.open=开放
|
||||||
feedback.state.rejected=拒绝
|
feedback.state.rejected=拒绝
|
||||||
feedback.title=标题
|
feedback.title=标题
|
||||||
feedback.type=类型
|
feedback.type=类型
|
||||||
feedback.type.bug_report=问题反馈
|
feedback.type.bug=问题反馈
|
||||||
feedback.type.feature_request=新功能请求
|
feedback.type.feature=新功能请求
|
||||||
feedback.unlike=反对
|
feedback.unlike=反对
|
||||||
feedback.version=启动器版本
|
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 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 ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
|
||||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
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,
|
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10,
|
||||||
TimeUnit.SECONDS);
|
TimeUnit.SECONDS);
|
||||||
private static final Pattern OAUTH_URL_PATTERN = Pattern
|
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 {
|
private static void getXBoxProfile(String uhs, String xstsToken) throws IOException {
|
||||||
HttpRequest.GET("https://profile.xboxlive.com/users/me/profile/settings",
|
HttpRequest.GET("https://profile.xboxlive.com/users/me/profile/settings",
|
||||||
pair("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
|
pair("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
|
||||||
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
|
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
|
||||||
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
|
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
|
||||||
+ "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined"))
|
+ "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined"))
|
||||||
.accept("application/json")
|
.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();
|
.getString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MinecraftProfileResponse getMinecraftProfile(String tokenType, String accessToken)
|
private static MinecraftProfileResponse getMinecraftProfile(String tokenType, String accessToken)
|
||||||
throws IOException, AuthenticationException {
|
throws IOException, AuthenticationException {
|
||||||
HttpURLConnection conn = HttpRequest.GET("https://api.minecraftservices.com/minecraft/profile")
|
HttpURLConnection conn = HttpRequest.GET("https://api.minecraftservices.com/minecraft/profile")
|
||||||
.authorization(String.format("%s %s", tokenType, accessToken))
|
.authorization(tokenType, accessToken)
|
||||||
.createConnection();
|
.createConnection();
|
||||||
int responseCode = conn.getResponseCode();
|
int responseCode = conn.getResponseCode();
|
||||||
if (responseCode == HTTP_NOT_FOUND) {
|
if (responseCode == HTTP_NOT_FOUND) {
|
||||||
@@ -341,27 +342,27 @@ public class MicrosoftService {
|
|||||||
* redirect URI used to obtain the authorization
|
* redirect URI used to obtain the authorization
|
||||||
* code.","correlation_id":"??????"}
|
* code.","correlation_id":"??????"}
|
||||||
*/
|
*/
|
||||||
private static class LiveAuthorizationResponse {
|
public static class LiveAuthorizationResponse {
|
||||||
@SerializedName("token_type")
|
@SerializedName("token_type")
|
||||||
String tokenType;
|
public String tokenType;
|
||||||
|
|
||||||
@SerializedName("expires_in")
|
@SerializedName("expires_in")
|
||||||
int expiresIn;
|
public int expiresIn;
|
||||||
|
|
||||||
@SerializedName("scope")
|
@SerializedName("scope")
|
||||||
String scope;
|
public String scope;
|
||||||
|
|
||||||
@SerializedName("access_token")
|
@SerializedName("access_token")
|
||||||
String accessToken;
|
public String accessToken;
|
||||||
|
|
||||||
@SerializedName("refresh_token")
|
@SerializedName("refresh_token")
|
||||||
String refreshToken;
|
public String refreshToken;
|
||||||
|
|
||||||
@SerializedName("user_id")
|
@SerializedName("user_id")
|
||||||
String userId;
|
public String userId;
|
||||||
|
|
||||||
@SerializedName("foci")
|
@SerializedName("foci")
|
||||||
String foci;
|
public String foci;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LiveRefreshResponse {
|
private static class LiveRefreshResponse {
|
||||||
@@ -391,14 +392,13 @@ public class MicrosoftService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Success Response: { "IssueInstant":"2020-12-07T19:52:08.4463796Z",
|
* Success Response: { "IssueInstant":"2020-12-07T19:52:08.4463796Z",
|
||||||
* "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{
|
* "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{
|
||||||
* "xui":[ { "uhs":"userhash" } ] } }
|
* "xui":[ { "uhs":"userhash" } ] } }
|
||||||
*
|
* <p>
|
||||||
* Error response: { "Identity":"0", "XErr":2148916238, "Message":"",
|
* Error response: { "Identity":"0", "XErr":2148916238, "Message":"",
|
||||||
* "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" }
|
* "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" }
|
||||||
*
|
* <p>
|
||||||
* XErr Candidates: 2148916233 = missing XBox account 2148916238 = child account
|
* XErr Candidates: 2148916233 = missing XBox account 2148916238 = child account
|
||||||
* not linked to a family
|
* not linked to a family
|
||||||
*/
|
*/
|
||||||
@@ -530,6 +530,10 @@ public class MicrosoftService {
|
|||||||
* @throws ExecutionException if an I/O error occurred.
|
* @throws ExecutionException if an I/O error occurred.
|
||||||
*/
|
*/
|
||||||
String waitFor() throws InterruptedException, ExecutionException;
|
String waitFor() throws InterruptedException, ExecutionException;
|
||||||
|
|
||||||
|
default String getIdToken() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = new GsonBuilder()
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ public abstract class HttpRequest {
|
|||||||
return header("Authorization", token);
|
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) {
|
public HttpRequest header(String key, String value) {
|
||||||
headers.put(key, value);
|
headers.put(key, value);
|
||||||
return this;
|
return this;
|
||||||
@@ -189,4 +197,9 @@ public abstract class HttpRequest {
|
|||||||
return new HttpPostRequest(url);
|
return new HttpPostRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface Authorization {
|
||||||
|
String getTokenType();
|
||||||
|
|
||||||
|
String getAccessToken();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user