feat(account): show little-skin login method by default. Closes #1622.

This commit is contained in:
huanghongxun 2022-08-20 14:58:38 +08:00
parent 66642fe621
commit 11e2277c9e
12 changed files with 137 additions and 58 deletions

View File

@ -81,6 +81,7 @@ public final class Accounts {
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
@ -96,14 +97,24 @@ public final class Accounts {
}
public static String getLoginType(AccountFactory<?> factory) {
return Optional.ofNullable(factory2type.get(factory))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
String type = factory2type.get(factory);
if (type != null) return type;
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
return factory2type.get(FACTORY_AUTHLIB_INJECTOR);
}
throw new IllegalArgumentException("Unrecognized account factory");
}
public static AccountFactory<?> getAccountFactory(String loginType) {
return Optional.ofNullable(type2factory.get(loginType))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
}
public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) {
return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server);
}
// ====
public static AccountFactory<?> getAccountFactory(Account account) {
@ -119,10 +130,10 @@ public final class Accounts {
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@ -231,6 +242,14 @@ public final class Accounts {
triggerAuthlibInjectorUpdateCheck();
}
Schedulers.io().execute(() -> {
try {
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
}
});
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@ -313,7 +332,7 @@ public final class Accounts {
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
private static final Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),

View File

@ -122,6 +122,14 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
AdvancedListItem littleSkinItem = new AdvancedListItem();
littleSkinItem.getStyleClass().add("navigation-drawer-item");
littleSkinItem.setActionButtonVisible(false);
littleSkinItem.setTitle(i18n("account.methods.little_skin"));
littleSkinItem.setLeftGraphic(wrap(SVG::server));
littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN)));
boxMethods.getChildren().add(littleSkinItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();

View File

@ -40,6 +40,7 @@ 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.authlibinjector.BoundAuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
@ -194,9 +195,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
setPrefWidth(560);
}
public CreateAccountPane(AuthlibInjectorServer authserver) {
this(Accounts.FACTORY_AUTHLIB_INJECTOR);
((AccountDetailsInputPane) detailsPane).selectAuthServer(authserver);
public CreateAccountPane(AuthlibInjectorServer authServer) {
this(Accounts.getAccountFactoryByAuthlibInjectorServer(authServer));
}
private void onAccept() {
@ -338,7 +338,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private static class AccountDetailsInputPane extends GridPane {
// ==== authlib-injector hyperlinks ====
private static final String[] ALLOWED_LINKS = { "register" };
private static final String[] ALLOWED_LINKS = { "homepage", "register" };
private static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
if (server == null) {
@ -360,12 +360,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
// =====
private AccountFactory<?> factory;
private @Nullable JFXComboBox<AuthlibInjectorServer> cboServers;
private final AccountFactory<?> factory;
private @Nullable AuthlibInjectorServer server;
private @Nullable JFXTextField txtUsername;
private @Nullable JFXPasswordField txtPassword;
private @Nullable JFXTextField txtUUID;
private BooleanBinding valid;
private final BooleanBinding valid;
public AccountDetailsInputPane(AccountFactory<?> factory, Runnable onAction) {
this.factory = factory;
@ -383,45 +383,33 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
int rowIndex = 0;
if (factory instanceof AuthlibInjectorAccountFactory) {
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
this.server = ((BoundAuthlibInjectorAccountFactory) factory).getServer();
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);
HBox.setMargin(cboServers, new Insets(0, 10, 0, 0));
cboServers.setMaxWidth(Double.MAX_VALUE);
Label lblServerName = new Label(this.server.getName());
lblServerName.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(lblServerName, Priority.ALWAYS);
HBox linksContainer = new HBox();
linksContainer.setAlignment(Pos.CENTER);
onChangeAndOperate(cboServers.valueProperty(), server -> linksContainer.getChildren().setAll(createHyperlinks(server)));
linksContainer.getChildren().setAll(createHyperlinks(this.server));
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = new JFXButton();
btnAddServer.setGraphic(SVG.plus(Theme.blackFillBinding(), 20, 20));
btnAddServer.getStyleClass().add("toggle-icon4");
btnAddServer.setOnAction(e -> {
Controllers.dialog(new AddAuthlibInjectorServerPane());
});
HBox boxServers = new HBox(cboServers, linksContainer, btnAddServer);
HBox boxServers = new HBox(lblServerName, linksContainer);
boxServers.setAlignment(Pos.CENTER_LEFT);
add(boxServers, 1, rowIndex);
rowIndex++;
}
if (factory instanceof AuthlibInjectorAccountFactory) {
throw new IllegalArgumentException("Use BoundAuthlibInjectorAccountFactory instead");
}
if (factory.getLoginType().requiresUsername) {
Label lblUsername = new Label(i18n("account.username"));
setHalignment(lblUsername, HPos.LEFT);
@ -523,8 +511,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
valid = new BooleanBinding() {
{
if (cboServers != null)
bind(cboServers.valueProperty());
if (txtUsername != null)
bind(txtUsername.textProperty());
if (txtPassword != null)
@ -535,8 +521,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
@Override
protected boolean computeValue() {
if (cboServers != null && cboServers.getValue() == null)
return false;
if (txtUsername != null && !txtUsername.validate())
return false;
if (txtPassword != null && !txtPassword.validate())
@ -551,11 +535,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
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;
}
} else if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) {
return !server.isNonEmailLogin();
}
return false;
}
@ -572,7 +553,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
public @Nullable AuthlibInjectorServer getAuthServer() {
return cboServers == null ? null : cboServers.getValue();
return this.server;
}
public @Nullable String getUsername() {
@ -587,10 +568,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
return valid;
}
public void selectAuthServer(AuthlibInjectorServer authserver) {
cboServers.getSelectionModel().select(authserver);
}
public void focus() {
if (txtUsername != null) {
txtUsername.requestFocus();

View File

@ -78,6 +78,7 @@ account.hmcl.hint=You need to click on "Login" and complete the process in the o
account.injector.add=Add an Authentication Server
account.injector.empty=None (You can click on the plus button on the right to add one)
account.injector.http=Warning\: This server uses the unsafe HTTP protocol, anyone between your connection will be able to see your credentials in cleartext.
account.injector.link.homepage=Homepage
account.injector.link.register=Register
account.injector.server=Authentication Server
account.injector.server_url=Server URL
@ -90,6 +91,7 @@ account.register=Register
account.manage=Account List
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.little_skin=Little Skin
account.methods.microsoft=Microsoft Account
account.methods.microsoft.birth=How to update your account birthday
account.methods.microsoft.close_page=Microsoft account authorization is now completed. \n\

View File

@ -90,6 +90,7 @@ account.register=Registrarse
account.manage=Lista de cuentas
account.methods=Tipo de inicio de sesión
account.methods.authlib_injector=authlib-injector
account.methods.little_skin=Little Skin
account.methods.microsoft=Cuenta Microsoft
account.methods.microsoft.birth=Cómo actualizar la fecha de nacimiento de tu cuenta
account.methods.microsoft.close_page=La autorización de la cuenta de Microsoft ha finalizado. \n\

View File

@ -88,6 +88,7 @@ account.register=登録
account.manage=アカウントリスト
account.methods=ログインタイプ
account.methods.authlib_injector=authlib-インジェクター
account.methods.little_skin=Little Skin
account.methods.microsoft=Microsoft Account
account.methods.microsoft.birth=誕生日の設定を編集する方法...
account.methods.microsoft.deauthorize=アカウントのバインドを解除

View File

@ -88,6 +88,7 @@ account.register=Регистрация
account.manage=Список аккаунтов
account.methods=Метод авторизации
account.methods.authlib_injector=authlib-injector
account.methods.little_skin=Little Skin
account.methods.microsoft=Аккаунт Microsoft
account.methods.microsoft.birth=Как изменить настройки дня рождения...
account.methods.microsoft.deauthorize=Отменить авторизацию аккаунта

View File

@ -76,6 +76,7 @@ account.hmcl.hint=你需要點擊“登入”按鈕,並在打開的網頁中
account.injector.add=新增認證伺服器
account.injector.empty=無 (按一下右側 + 加入)
account.injector.http=警告: 此伺服器使用不安全的 HTTP 協定,您的密碼在登入時會被明文傳輸。
account.injector.link.homepage=首頁
account.injector.link.register=註冊
account.injector.server=認證伺服器
account.injector.server_url=伺服器位址
@ -88,6 +89,7 @@ account.register=註冊
account.manage=帳戶列表
account.methods=登入方式
account.methods.authlib_injector=authlib-injector 登入
account.methods.little_skin=Little Skin
account.methods.microsoft=微軟帳戶
account.methods.microsoft.birth=如何修改帳戶出生日期
account.methods.microsoft.deauthorize=解除帳戶授權

View File

@ -76,6 +76,7 @@ account.hmcl.hint=你需要点击“登录”按钮,并在打开的网页中
account.injector.add=添加认证服务器
account.injector.empty=无(点击右侧加号添加)
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输!
account.injector.link.homepage=主页
account.injector.link.register=注册
account.injector.server=认证服务器
account.injector.server_url=服务器地址
@ -88,6 +89,7 @@ account.register=注册
account.manage=帐户列表
account.methods=登录方式
account.methods.authlib_injector=外置登录 (authlib-injector)
account.methods.little_skin=Little Skin
account.methods.microsoft=微软帐户
account.methods.microsoft.birth=如何修改帐户出生日期
account.methods.microsoft.close_page=已完成微软帐户授权,接下来启动器还需要完成剩余登录步骤。你已经可以关闭本页面了。

View File

@ -33,8 +33,8 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private AuthlibInjectorArtifactProvider downloader;
private Function<String, AuthlibInjectorServer> serverLookup;
private final AuthlibInjectorArtifactProvider downloader;
private final Function<String, AuthlibInjectorServer> serverLookup;
/**
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
@ -64,14 +64,17 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
public AuthlibInjectorAccount fromStorage(Map<Object, Object> storage) {
Objects.requireNonNull(storage);
String apiRoot = tryCast(storage.get("serverBaseURL"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have API root."));
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return fromStorage(storage, downloader, server);
}
static AuthlibInjectorAccount fromStorage(Map<Object, Object> storage, AuthlibInjectorArtifactProvider downloader, AuthlibInjectorServer server) {
YggdrasilSession session = YggdrasilSession.fromStorage(storage);
String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
String apiRoot = tryCast(storage.get("serverBaseURL"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have API root."));
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
it -> {

View File

@ -0,0 +1,61 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import java.util.Map;
import java.util.Objects;
public class BoundAuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final AuthlibInjectorArtifactProvider downloader;
private final AuthlibInjectorServer server;
/**
* @param server Authlib-Injector Server
*/
public BoundAuthlibInjectorAccountFactory(AuthlibInjectorArtifactProvider downloader, AuthlibInjectorServer server) {
this.downloader = downloader;
this.server = server;
}
@Override
public AccountLoginType getLoginType() {
return AccountLoginType.USERNAME_PASSWORD;
}
public AuthlibInjectorServer getServer() {
return server;
}
@Override
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
Objects.requireNonNull(username);
Objects.requireNonNull(password);
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
}
@Override
public AuthlibInjectorAccount fromStorage(Map<Object, Object> storage) {
return AuthlibInjectorAccountFactory.fromStorage(storage, downloader, server);
}
}

View File

@ -78,6 +78,8 @@ public class YggdrasilSession {
}
public static YggdrasilSession fromStorage(Map<?, ?> storage) {
Objects.requireNonNull(storage);
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));