mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-03-01 17:25:53 +08:00
wip: account refactor
This commit is contained in:
parent
feb15a3c64
commit
665b0c4390
@ -27,16 +27,22 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterDeletedException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.*;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
@ -322,4 +328,47 @@ public final class Accounts {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
|
||||
}
|
||||
// ====
|
||||
|
||||
public static String localizeErrorMessage(Exception exception) {
|
||||
if (exception instanceof NoCharacterException) {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
} else if (exception instanceof ServerResponseMalformedException) {
|
||||
return i18n("account.failed.server_response_malformed");
|
||||
} else if (exception instanceof RemoteAuthenticationException) {
|
||||
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
|
||||
String remoteMessage = remoteException.getRemoteMessage();
|
||||
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("Invalid credentials"))
|
||||
return i18n("account.failed.invalid_credentials");
|
||||
else if (remoteMessage.contains("Invalid token"))
|
||||
return i18n("account.failed.invalid_token");
|
||||
else if (remoteMessage.contains("Invalid username or password"))
|
||||
return i18n("account.failed.invalid_password");
|
||||
else
|
||||
return remoteMessage;
|
||||
}
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception instanceof InvalidSkinException) {
|
||||
return i18n("account.skin.invalid_skin");
|
||||
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
|
||||
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
|
||||
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
|
||||
return i18n("account.methods.microsoft.error.missing_xbox_account");
|
||||
} else {
|
||||
return i18n("account.methods.microsoft.error.unknown", errorCode);
|
||||
}
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -426,4 +426,10 @@ public final class SVG {
|
||||
"M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node account(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z",
|
||||
fill, width, height);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@ -176,7 +176,7 @@ public class AccountListItem extends RadioButton {
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
|
||||
Controllers.dialog(Accounts.localizeErrorMessage(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -27,10 +27,8 @@ import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
@ -75,10 +73,31 @@ public class AccountListPage extends ListPageBase<AccountListItem> implements De
|
||||
|
||||
{
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.startCategory(i18n("account.create"))
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("account.methods.offline"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.account(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("account.methods.yggdrasil"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.mojang(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("account.methods.microsoft"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.microsoft(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("account.methods.authlib_injector"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.gear(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_AUTHLIB_INJECTOR)));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("account.create"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.plusCircleOutline(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new AddAccountPane()));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.plus(null, 20, 20)));
|
||||
settingsItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane()));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
root.setLeft(sideBar);
|
||||
|
@ -25,6 +25,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@ -74,7 +75,7 @@ public class AccountLoginPane extends StackPane {
|
||||
if (e instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblCreationWarning.setText(AddAccountPane.accountException(e));
|
||||
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
}).start();
|
||||
|
@ -1,404 +0,0 @@
|
||||
/*
|
||||
* 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.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXPasswordField;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AddAccountPane extends StackPane {
|
||||
|
||||
@FXML
|
||||
private JFXTextField txtUsername;
|
||||
@FXML
|
||||
private JFXPasswordField txtPassword;
|
||||
@FXML
|
||||
private Label lblCreationWarning;
|
||||
@FXML
|
||||
private Label lblPassword;
|
||||
@FXML
|
||||
private Label lblUsername;
|
||||
@FXML
|
||||
private StackPane tabHeaderPane;
|
||||
@FXML
|
||||
private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
@FXML
|
||||
private Label lblInjectorServer;
|
||||
@FXML
|
||||
private JFXButton btnAccept;
|
||||
@FXML
|
||||
private JFXButton btnAddServer;
|
||||
@FXML
|
||||
private JFXButton btnManageServer;
|
||||
@FXML
|
||||
private SpinnerPane acceptPane;
|
||||
@FXML
|
||||
private HBox linksContainer;
|
||||
@FXML
|
||||
private GridPane body;
|
||||
|
||||
private final TabHeader tabHeader;
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
private ListProperty<Hyperlink> links = new SimpleListProperty<>();
|
||||
|
||||
private final Map<AccountFactory<?>, TabControl.Tab<Node>> tabMap = new HashMap<>();
|
||||
|
||||
public AddAccountPane() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
|
||||
|
||||
List<TabControl.Tab<Node>> tabs = new ArrayList<>();
|
||||
for (AccountFactory<?> factory : Accounts.FACTORIES) {
|
||||
TabControl.Tab<Node> tab = new TabControl.Tab<>(factory.getLoginType().name(), Accounts.getLocalizedLoginTypeName(factory));
|
||||
tab.setUserData(factory);
|
||||
tabMap.put(factory, tab);
|
||||
tabs.add(tab);
|
||||
}
|
||||
|
||||
tabHeader = new TabHeader(tabs.toArray(new TabControl.Tab[0]));
|
||||
tabHeader.getStyleClass().add("add-account-tab-header");
|
||||
// try selecting the preferred login type
|
||||
tabHeader.getSelectionModel().select(
|
||||
tabMap.get(tabMap.keySet().stream().filter(factory -> Accounts.getLoginType(factory).equals(config().getPreferredLoginType()))
|
||||
.findFirst().orElse(Accounts.FACTORY_OFFLINE))
|
||||
);
|
||||
|
||||
tabHeaderPane.getChildren().setAll(tabHeader);
|
||||
tabHeader.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
cboServers.getItems().addListener(onInvalidating(this::resetServerSelection));
|
||||
resetServerSelection();
|
||||
|
||||
btnAddServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
btnManageServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
|
||||
cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer));
|
||||
checkIfNoServer();
|
||||
|
||||
ReadOnlyObjectProperty<TabHeader.Tab<?>> loginType = tabHeader.getSelectionModel().selectedItemProperty();
|
||||
|
||||
// remember the last used login type
|
||||
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType((AccountFactory<?>) newValue.getUserData())));
|
||||
|
||||
txtUsername.visibleProperty().bind(Bindings.createBooleanBinding(() -> ((AccountFactory<?>) loginType.get().getUserData()).getLoginType().requiresUsername, loginType));
|
||||
lblUsername.visibleProperty().bind(txtUsername.visibleProperty());
|
||||
txtPassword.visibleProperty().bind(Bindings.createBooleanBinding(() -> ((AccountFactory<?>) loginType.get().getUserData()).getLoginType().requiresPassword, loginType));
|
||||
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
|
||||
|
||||
cboServers.visibleProperty().bind(loginType.isEqualTo(tabMap.get(Accounts.FACTORY_AUTHLIB_INJECTOR)));
|
||||
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
|
||||
|
||||
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !( // consider the opposite situation: input is valid
|
||||
(!txtUsername.isVisible() || txtUsername.validate()) &&
|
||||
// invisible means the field is not needed, neither should it be validated
|
||||
(!txtPassword.isVisible() || txtPassword.validate()) &&
|
||||
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
|
||||
),
|
||||
txtUsername.textProperty(), txtPassword.textProperty(),
|
||||
loginType, cboServers.getSelectionModel().selectedItemProperty(),
|
||||
txtPassword.visibleProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
.map(AddAccountPane::createHyperlinks)
|
||||
.map(FXCollections::observableList));
|
||||
Bindings.bindContent(linksContainer.getChildren(), links);
|
||||
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
|
||||
onEscPressed(this, this::onCreationCancel);
|
||||
}
|
||||
|
||||
private boolean validateUsername(String username) {
|
||||
AccountFactory<?> loginType = ((AccountFactory<?>) tabHeader.getSelectionModel().getSelectedItem().getUserData());
|
||||
if (loginType == Accounts.FACTORY_OFFLINE) {
|
||||
return true;
|
||||
} else if (loginType == Accounts.FACTORY_AUTHLIB_INJECTOR) {
|
||||
AuthlibInjectorServer server = cboServers.getSelectionModel().getSelectedItem();
|
||||
if (server != null && server.isNonEmailLogin()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return username.contains("@");
|
||||
}
|
||||
|
||||
private static final String[] ALLOWED_LINKS = {"register"};
|
||||
|
||||
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
if (server == null) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
Map<String, String> links = server.getLinks();
|
||||
List<Hyperlink> result = new ArrayList<>();
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
}
|
||||
}
|
||||
return unmodifiableList(result);
|
||||
}
|
||||
|
||||
private void resetServerSelection() {
|
||||
if (!cboServers.getItems().isEmpty()) {
|
||||
Platform.runLater(() -> {
|
||||
// the selection will not be updated as expected
|
||||
// if we call it immediately
|
||||
cboServers.getSelectionModel().selectFirst();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIfNoServer() {
|
||||
if (cboServers.getItems().isEmpty())
|
||||
cboServers.getStyleClass().setAll("jfx-combo-box-warning");
|
||||
else
|
||||
cboServers.getStyleClass().setAll("jfx-combo-box");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
|
||||
*/
|
||||
private Object getAuthAdditionalData() {
|
||||
AccountFactory<?> factory = ((AccountFactory<?>) tabHeader.getSelectionModel().getSelectedItem().getUserData());
|
||||
if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) {
|
||||
return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCreationAccept() {
|
||||
if (btnAccept.isDisabled())
|
||||
return;
|
||||
|
||||
acceptPane.showSpinner();
|
||||
lblCreationWarning.setText("");
|
||||
body.setDisable(true);
|
||||
|
||||
String username = txtUsername.getText();
|
||||
String password = txtPassword.getText();
|
||||
AccountFactory<?> factory = ((AccountFactory<?>) tabHeader.getSelectionModel().getSelectedItem().getUserData());
|
||||
Object additionalData = getAuthAdditionalData();
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new Selector(), username, password, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
// adding an already-added account
|
||||
// instead of discarding the new account, we first remove the existing one then add the new one
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
|
||||
// select the new account
|
||||
Accounts.setSelectedAccount(account);
|
||||
|
||||
acceptPane.hideSpinner();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, exception -> {
|
||||
if (exception instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblCreationWarning.setText(accountException(exception));
|
||||
}
|
||||
setDisable(false);
|
||||
acceptPane.hideSpinner();
|
||||
}).executor(true);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCreationCancel() {
|
||||
if (loginTask != null) {
|
||||
loginTask.cancel();
|
||||
}
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onManageInjecterServers() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(Controllers.getServersPage());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAddInjecterServer() {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
}
|
||||
|
||||
private static class Selector extends BorderPane implements CharacterSelector {
|
||||
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXButton cancel = new JFXButton();
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private GameProfile selectedProfile = null;
|
||||
|
||||
public Selector() {
|
||||
setStyle("-fx-padding: 8px;");
|
||||
|
||||
cancel.setText(i18n("button.cancel"));
|
||||
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
|
||||
cancel.setOnMouseClicked(e -> latch.countDown());
|
||||
|
||||
listBox.startCategory(i18n("account.choose"));
|
||||
|
||||
setCenter(listBox);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
hbox.getChildren().add(cancel);
|
||||
setBottom(hbox);
|
||||
|
||||
onEscPressed(this, cancel::fire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
Controllers.dialog(this);
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
|
||||
if (selectedProfile == null)
|
||||
throw new NoSelectedCharacterException();
|
||||
|
||||
return selectedProfile;
|
||||
} catch (InterruptedException ignore) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} finally {
|
||||
runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String accountException(Exception exception) {
|
||||
if (exception instanceof NoCharacterException) {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
} else if (exception instanceof ServerResponseMalformedException) {
|
||||
return i18n("account.failed.server_response_malformed");
|
||||
} else if (exception instanceof RemoteAuthenticationException) {
|
||||
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
|
||||
String remoteMessage = remoteException.getRemoteMessage();
|
||||
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("Invalid credentials"))
|
||||
return i18n("account.failed.invalid_credentials");
|
||||
else if (remoteMessage.contains("Invalid token"))
|
||||
return i18n("account.failed.invalid_token");
|
||||
else if (remoteMessage.contains("Invalid username or password"))
|
||||
return i18n("account.failed.invalid_password");
|
||||
else
|
||||
return remoteMessage;
|
||||
}
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception instanceof InvalidSkinException) {
|
||||
return i18n("account.skin.invalid_skin");
|
||||
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
|
||||
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
|
||||
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
|
||||
return i18n("account.methods.microsoft.error.missing_xbox_account");
|
||||
} else {
|
||||
return i18n("account.methods.microsoft.error.unknown", errorCode);
|
||||
}
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,514 @@
|
||||
/*
|
||||
* 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.ui.account;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static javafx.beans.binding.Bindings.bindContent;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onChange;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onChangeAndOperate;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.setValidateWhileTextChanged;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.classPropertyFor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabControl;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXPasswordField;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class CreateAccountPane extends JFXDialogLayout {
|
||||
|
||||
private boolean showMethodSwitcher;
|
||||
private AccountFactory<?> factory;
|
||||
|
||||
private Label lblErrorMessage;
|
||||
private JFXButton btnAccept;
|
||||
private SpinnerPane spinner;
|
||||
private JFXButton btnCancel;
|
||||
private Node body;
|
||||
|
||||
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
|
||||
private Pane detailsContainer;
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
public CreateAccountPane() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public CreateAccountPane(AccountFactory<?> factory) {
|
||||
if (factory == null) {
|
||||
showMethodSwitcher = true;
|
||||
String preferred = config().getPreferredLoginType();
|
||||
try {
|
||||
factory = Accounts.getAccountFactory(preferred);
|
||||
} catch (IllegalArgumentException e) {
|
||||
factory = Accounts.FACTORY_OFFLINE;
|
||||
}
|
||||
} else {
|
||||
showMethodSwitcher = false;
|
||||
}
|
||||
this.factory = factory;
|
||||
|
||||
{
|
||||
String title;
|
||||
if (showMethodSwitcher) {
|
||||
title = "account.create";
|
||||
} else {
|
||||
title = "account.create." + Accounts.getLoginType(factory);
|
||||
}
|
||||
setHeading(new Label(i18n(title)));
|
||||
}
|
||||
|
||||
{
|
||||
lblErrorMessage = new Label();
|
||||
|
||||
btnAccept = new JFXButton(i18n("button.ok"));
|
||||
btnAccept.getStyleClass().add("dialog-accept");
|
||||
btnAccept.setOnAction(e -> onAccept());
|
||||
|
||||
spinner = new SpinnerPane();
|
||||
spinner.getStyleClass().add("small-spinner-pane");
|
||||
spinner.setContent(btnAccept);
|
||||
|
||||
btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
btnCancel.setOnAction(e -> onCancel());
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
|
||||
setActions(lblErrorMessage, new HBox(spinner, btnCancel));
|
||||
}
|
||||
|
||||
if (showMethodSwitcher) {
|
||||
TabControl.Tab<?>[] tabs = new TabControl.Tab[Accounts.FACTORIES.size()];
|
||||
TabControl.Tab<?> selected = null;
|
||||
for (int i = 0; i < tabs.length; i++) {
|
||||
AccountFactory<?> f = Accounts.FACTORIES.get(i);
|
||||
tabs[i] = new TabControl.Tab<>(Accounts.getLoginType(f), Accounts.getLocalizedLoginTypeName(f));
|
||||
tabs[i].setUserData(f);
|
||||
if (factory == f) {
|
||||
selected = tabs[i];
|
||||
}
|
||||
}
|
||||
|
||||
TabHeader tabHeader = new TabHeader(tabs);
|
||||
tabHeader.getStyleClass().add("add-account-tab-header");
|
||||
tabHeader.setMinWidth(USE_PREF_SIZE);
|
||||
tabHeader.setMaxWidth(USE_PREF_SIZE);
|
||||
tabHeader.getSelectionModel().select(selected);
|
||||
onChange(tabHeader.getSelectionModel().selectedItemProperty(),
|
||||
newItem -> {
|
||||
if (newItem == null)
|
||||
return;
|
||||
AccountFactory<?> newMethod = (AccountFactory<?>) newItem.getUserData();
|
||||
config().setPreferredLoginType(Accounts.getLoginType(newMethod));
|
||||
this.factory = newMethod;
|
||||
initDetailsPane();
|
||||
});
|
||||
|
||||
detailsContainer = new StackPane();
|
||||
detailsContainer.setPadding(new Insets(15, 0, 0, 0));
|
||||
|
||||
VBox boxBody = new VBox(tabHeader, detailsContainer);
|
||||
boxBody.setAlignment(Pos.CENTER);
|
||||
body = boxBody;
|
||||
setBody(body);
|
||||
|
||||
} else {
|
||||
detailsContainer = new StackPane();
|
||||
detailsContainer.setPadding(new Insets(10, 0, 0, 0));
|
||||
body = detailsContainer;
|
||||
setBody(body);
|
||||
}
|
||||
initDetailsPane();
|
||||
|
||||
setPrefWidth(560);
|
||||
}
|
||||
|
||||
private void onAccept() {
|
||||
spinner.showSpinner();
|
||||
lblErrorMessage.setText("");
|
||||
body.setDisable(true);
|
||||
|
||||
String username;
|
||||
String password;
|
||||
Object additionalData;
|
||||
if (detailsPane instanceof AccountDetailsInputPane) {
|
||||
AccountDetailsInputPane details = (AccountDetailsInputPane) detailsPane;
|
||||
username = details.getUsername();
|
||||
password = details.getPassword();
|
||||
additionalData = details.getAuthServer();
|
||||
} else {
|
||||
username = null;
|
||||
password = null;
|
||||
additionalData = null;
|
||||
}
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
// adding an already-added account
|
||||
// instead of discarding the new account, we first remove the existing one then add the new one
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
|
||||
// select the new account
|
||||
Accounts.setSelectedAccount(account);
|
||||
|
||||
spinner.hideSpinner();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, exception -> {
|
||||
if (exception instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblErrorMessage.setText(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}).executor(true);
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
if (loginTask != null) {
|
||||
loginTask.cancel();
|
||||
}
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
private void initDetailsPane() {
|
||||
if (detailsPane != null) {
|
||||
btnAccept.disableProperty().unbind();
|
||||
detailsContainer.getChildren().remove(detailsPane);
|
||||
lblErrorMessage.setText("");
|
||||
}
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
Label lblTip = new Label(i18n("account.methods.microsoft.manual")); // TODO
|
||||
lblTip.setWrapText(true);
|
||||
detailsPane = lblTip;
|
||||
btnAccept.setDisable(false);
|
||||
} else {
|
||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
|
||||
}
|
||||
detailsContainer.getChildren().add(detailsPane);
|
||||
}
|
||||
|
||||
private static class AccountDetailsInputPane extends GridPane {
|
||||
|
||||
// ==== authlib-injector hyperlinks ====
|
||||
private static final String[] ALLOWED_LINKS = { "register" };
|
||||
|
||||
private static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
if (server == null) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
Map<String, String> links = server.getLinks();
|
||||
List<Hyperlink> result = new ArrayList<>();
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
}
|
||||
}
|
||||
return unmodifiableList(result);
|
||||
}
|
||||
// =====
|
||||
|
||||
private AccountFactory<?> factory;
|
||||
private @Nullable JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
private @Nullable JFXTextField txtUsername;
|
||||
private @Nullable JFXPasswordField txtPassword;
|
||||
private BooleanBinding valid;
|
||||
|
||||
public AccountDetailsInputPane(AccountFactory<?> factory, Runnable onAction) {
|
||||
this.factory = factory;
|
||||
|
||||
setVgap(15);
|
||||
setHgap(15);
|
||||
setAlignment(Pos.CENTER);
|
||||
|
||||
ColumnConstraints col0 = new ColumnConstraints();
|
||||
col0.setMinWidth(USE_PREF_SIZE);
|
||||
getColumnConstraints().add(col0);
|
||||
ColumnConstraints col1 = new ColumnConstraints();
|
||||
col1.setHgrow(Priority.ALWAYS);
|
||||
getColumnConstraints().add(col1);
|
||||
|
||||
int rowIndex = 0;
|
||||
|
||||
if (factory instanceof AuthlibInjectorAccountFactory) {
|
||||
Label lblServers = new Label(i18n("account.injector.server"));
|
||||
setHalignment(lblServers, HPos.LEFT);
|
||||
add(lblServers, 0, rowIndex);
|
||||
|
||||
cboServers = new JFXComboBox<>();
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
cboServers.getItems().addListener(onInvalidating(
|
||||
() -> Platform.runLater( // the selection will not be updated as expected if we call it immediately
|
||||
cboServers.getSelectionModel()::selectFirst)));
|
||||
cboServers.getSelectionModel().selectFirst();
|
||||
cboServers.setPromptText(i18n("account.injector.empty"));
|
||||
BooleanBinding noServers = createBooleanBinding(cboServers.getItems()::isEmpty, cboServers.getItems());
|
||||
classPropertyFor(cboServers, "jfx-combo-box-warning").bind(noServers);
|
||||
classPropertyFor(cboServers, "jfx-combo-box").bind(noServers.not());
|
||||
HBox.setHgrow(cboServers, Priority.ALWAYS);
|
||||
cboServers.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
HBox linksContainer = new HBox();
|
||||
linksContainer.setAlignment(Pos.CENTER);
|
||||
linksContainer.setPadding(new Insets(0, 5, 0, 15));
|
||||
onChangeAndOperate(cboServers.valueProperty(), server -> linksContainer.getChildren().setAll(createHyperlinks(server)));
|
||||
linksContainer.setMinWidth(USE_PREF_SIZE);
|
||||
|
||||
JFXButton btnAddServer = new JFXButton();
|
||||
btnAddServer.setGraphic(SVG.plus(null, 20, 20));
|
||||
btnAddServer.getStyleClass().add("toggle-icon4");
|
||||
btnAddServer.setOnAction(e -> {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
});
|
||||
|
||||
JFXButton btnManageServers = new JFXButton();
|
||||
btnManageServers.setGraphic(SVG.gear(null, 20, 20));
|
||||
btnManageServers.getStyleClass().add("toggle-icon4");
|
||||
btnManageServers.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(Controllers.getServersPage());
|
||||
});
|
||||
|
||||
HBox boxServers = new HBox(cboServers, linksContainer, btnAddServer, btnManageServers);
|
||||
add(boxServers, 1, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory.getLoginType().requiresUsername) {
|
||||
Label lblUsername = new Label(i18n("account.username"));
|
||||
setHalignment(lblUsername, HPos.LEFT);
|
||||
add(lblUsername, 0, rowIndex);
|
||||
|
||||
txtUsername = new JFXTextField();
|
||||
txtUsername.setValidators(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("input.email"), username -> {
|
||||
if (requiresEmailAsUsername()) {
|
||||
return username.contains("@");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
setValidateWhileTextChanged(txtUsername, true);
|
||||
txtUsername.setOnAction(e -> onAction.run());
|
||||
add(txtUsername, 1, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory.getLoginType().requiresPassword) {
|
||||
Label lblPassword = new Label(i18n("account.password"));
|
||||
setHalignment(lblPassword, HPos.LEFT);
|
||||
add(lblPassword, 0, rowIndex);
|
||||
|
||||
txtPassword = new JFXPasswordField();
|
||||
txtPassword.setValidators(new RequiredValidator());
|
||||
setValidateWhileTextChanged(txtPassword, true);
|
||||
txtPassword.setOnAction(e -> onAction.run());
|
||||
add(txtPassword, 1, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
valid = new BooleanBinding() {
|
||||
{
|
||||
if (cboServers != null)
|
||||
bind(cboServers.valueProperty());
|
||||
if (txtUsername != null)
|
||||
bind(txtUsername.textProperty());
|
||||
if (txtPassword != null)
|
||||
bind(txtPassword.textProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean computeValue() {
|
||||
if (cboServers != null && cboServers.getValue() == null)
|
||||
return false;
|
||||
if (txtUsername != null && !txtUsername.validate())
|
||||
return false;
|
||||
if (txtPassword != null && !txtPassword.validate())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean requiresEmailAsUsername() {
|
||||
if (factory instanceof YggdrasilAccountFactory) {
|
||||
return true;
|
||||
} else if ((factory instanceof AuthlibInjectorAccountFactory) && cboServers != null) {
|
||||
AuthlibInjectorServer server = cboServers.getValue();
|
||||
if (server != null && !server.isNonEmailLogin()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable AuthlibInjectorServer getAuthServer() {
|
||||
return cboServers == null ? null : cboServers.getValue();
|
||||
}
|
||||
|
||||
public @Nullable String getUsername() {
|
||||
return txtUsername == null ? null : txtUsername.getText();
|
||||
}
|
||||
|
||||
public @Nullable String getPassword() {
|
||||
return txtPassword == null ? null : txtPassword.getText();
|
||||
}
|
||||
|
||||
public BooleanBinding validProperty() {
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DialogCharacterSelector extends BorderPane implements CharacterSelector {
|
||||
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXButton cancel = new JFXButton();
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private GameProfile selectedProfile = null;
|
||||
|
||||
public DialogCharacterSelector() {
|
||||
setStyle("-fx-padding: 8px;");
|
||||
|
||||
cancel.setText(i18n("button.cancel"));
|
||||
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
|
||||
cancel.setOnAction(e -> latch.countDown());
|
||||
|
||||
listBox.startCategory(i18n("account.choose"));
|
||||
|
||||
setCenter(listBox);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
hbox.getChildren().add(cancel);
|
||||
setBottom(hbox);
|
||||
|
||||
onEscPressed(this, cancel::fire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
Controllers.dialog(this);
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
|
||||
if (selectedProfile == null)
|
||||
throw new NoSelectedCharacterException();
|
||||
|
||||
return selectedProfile;
|
||||
} catch (InterruptedException ignored) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} finally {
|
||||
Platform.runLater(() -> fireEvent(new DialogCloseEvent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.account.AddAccountPane;
|
||||
import org.jackhuang.hmcl.ui.account.CreateAccountPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
@ -144,7 +144,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
Controllers.navigate(Controllers.getAccountListPage());
|
||||
|
||||
if (Accounts.getAccounts().isEmpty()) {
|
||||
Controllers.dialog(new AddAccountPane());
|
||||
Controllers.dialog(new CreateAccountPane());
|
||||
}
|
||||
});
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
@ -227,7 +227,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
}
|
||||
|
||||
private void addNewAccount() {
|
||||
Controllers.dialog(new AddAccountPane());
|
||||
Controllers.dialog(new CreateAccountPane());
|
||||
}
|
||||
// ====
|
||||
|
||||
|
@ -1,76 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.*?>
|
||||
<?import com.jfoenix.validation.RequiredFieldValidator?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.RequiredValidator?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
type="StackPane">
|
||||
<JFXDialogLayout fx:id="layout">
|
||||
<heading>
|
||||
<Label text="%account.create"/>
|
||||
</heading>
|
||||
<body>
|
||||
<GridPane fx:id="body" vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints maxWidth="70" minWidth="70"/>
|
||||
<ColumnConstraints/>
|
||||
<ColumnConstraints minWidth="140"/>
|
||||
</columnConstraints>
|
||||
|
||||
<StackPane fx:id="tabHeaderPane" GridPane.columnIndex="0" GridPane.columnSpan="3" GridPane.rowIndex="0" />
|
||||
|
||||
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="LEFT"
|
||||
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
|
||||
|
||||
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
|
||||
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||
<HBox fx:id="linksContainer" alignment="CENTER_LEFT"/>
|
||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||
<graphic>
|
||||
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<JFXButton fx:id="btnManageServer" styleClass="toggle-icon4" onMouseClicked="#onManageInjecterServers">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/gear.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
|
||||
<Label fx:id="lblUsername" text="%account.username" GridPane.halignment="LEFT" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
|
||||
<JFXTextField fx:id="txtUsername" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.columnSpan="2"
|
||||
FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
|
||||
<validators>
|
||||
<RequiredValidator />
|
||||
</validators>
|
||||
</JFXTextField>
|
||||
|
||||
<Label fx:id="lblPassword" text="%account.password" GridPane.halignment="LEFT" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
|
||||
|
||||
<JFXPasswordField fx:id="txtPassword" GridPane.columnIndex="1" GridPane.rowIndex="3"
|
||||
GridPane.columnSpan="2" FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
|
||||
<validators>
|
||||
<RequiredFieldValidator />
|
||||
</validators>
|
||||
</JFXPasswordField>
|
||||
</GridPane>
|
||||
</body>
|
||||
<actions>
|
||||
<Label fx:id="lblCreationWarning"/>
|
||||
<HBox>
|
||||
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
|
||||
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
|
||||
</SpinnerPane>
|
||||
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
|
||||
</HBox>
|
||||
</actions>
|
||||
</JFXDialogLayout>
|
||||
</fx:root>
|
@ -42,7 +42,11 @@ about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
|
||||
account=账户
|
||||
account.character=角色
|
||||
account.choose=选择一个角色
|
||||
account.create=新建账户
|
||||
account.create=添加账户
|
||||
account.create.microsoft=添加微软账户
|
||||
account.create.yggdrasil=添加 Mojang 账户
|
||||
account.create.offline=添加离线模式账户
|
||||
account.create.authlibInjector=添加外置登录账户 (authlib-injector)
|
||||
account.email=邮箱
|
||||
account.failed.character_deleted=此角色已被删除
|
||||
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
|
||||
@ -65,7 +69,7 @@ account.injector.server_name=服务器名称
|
||||
account.manage=账户列表
|
||||
account.methods=登录方式
|
||||
account.methods.authlib_injector=外置登录 (authlib-injector)
|
||||
account.methods.microsoft=微软登录
|
||||
account.methods.microsoft=微软账户
|
||||
account.methods.microsoft.close_page=已完成微软账号授权,接下来启动器还需要完成剩余登录步骤。你已经可以关闭本页面了。
|
||||
account.methods.microsoft.error.add_family=由于你未满 18 岁,你的账号必须被加入到家庭中才能登录游戏。
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微软账号尚未关联 XBox 账号,你必须先创建 XBox 账号,才能登录游戏。
|
||||
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="black" content="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath content="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" />
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<javafx.scene.shape.SVGPath fill="white" content="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z" />
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@ -22,6 +22,7 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.util.Objects;
|
||||
@ -30,6 +31,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/**
|
||||
@ -130,6 +132,28 @@ public final class ExtendedProperties {
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== General ====
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ObjectProperty<Boolean> classPropertyFor(Node node, String cssClass) {
|
||||
return (ObjectProperty<Boolean>) node.getProperties().computeIfAbsent(
|
||||
PROP_PREFIX + ".cssClass." + cssClass,
|
||||
any -> {
|
||||
ObservableList<String> classes = node.getStyleClass();
|
||||
return new ReadWriteComposedProperty<>(node, "extra.cssClass." + cssClass,
|
||||
createBooleanBinding(() -> classes.contains(cssClass), classes),
|
||||
state -> {
|
||||
if (state) {
|
||||
if (!classes.contains(cssClass)) {
|
||||
classes.add(cssClass);
|
||||
}
|
||||
} else {
|
||||
classes.remove(cssClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// ====
|
||||
|
||||
private ExtendedProperties() {
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user