wip: account refactor

This commit is contained in:
Haowei Wen 2021-09-03 13:00:47 +08:00
parent feb15a3c64
commit 665b0c4390
No known key found for this signature in database
GPG Key ID: 5BC167F73EA558E4
19 changed files with 632 additions and 519 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 账号,才能登录游戏。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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