Merge pull request #399 from yushijinhun/refactor-account

重构账户部分
This commit is contained in:
huanghongxun
2018-07-21 19:50:46 +08:00
committed by GitHub
22 changed files with 587 additions and 481 deletions

View File

@@ -1,57 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}

View File

@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}

View File

@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
@@ -42,7 +42,7 @@ public final class AccountHelper {
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
for (Account account : Settings.INSTANCE.getAccounts()) {
for (Account account : Accounts.getAccounts()) {
if (account instanceof YggdrasilAccount) {
new SkinLoadTask((YggdrasilAccount) account, false).start();
}

View File

@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* 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
@@ -29,14 +29,27 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -44,33 +57,160 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public final class Accounts {
private Accounts() {}
public static final String OFFLINE_ACCOUNT_KEY = "offline";
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
private static final String TYPE_OFFLINE = "offline";
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
else return YGGDRASIL_ACCOUNT_KEY;
private static Map<String, AccountFactory<?>> type2factory = mapOf(
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
private static String accountType(Account account) {
if (account instanceof OfflineAccount)
return TYPE_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return TYPE_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return TYPE_YGGDRASIL_ACCOUNT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
static String getAccountId(Account account) {
return getAccountId(account.getUsername(), account.getCharacter());
public static AccountFactory<?> getAccountFactory(Account account) {
return type2factory.get(accountType(account));
}
static String getAccountId(String username, String character) {
return username + ":" + character;
private static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListProperty<Account> accountsWrapper = new ReadOnlyListWrapper<>(accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
CONFIG.setSelectedAccount(selected == null ? "" : accountId(selected));
}
};
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
CONFIG.getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
})
.collect(toList()));
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#CONFIG}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
CONFIG.getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
});
initialized = true;
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(CONFIG.getSelectedAccount()))
.findFirst()
.orElse(null));
}
public static ObservableList<Account> getAccounts() {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper;
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
public static void setSelectedAccount(Account selectedAccount) {
Accounts.selectedAccount.set(selectedAccount);
}
public static ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
// ==== authlib-injector ====
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return CONFIG.getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
@@ -90,4 +230,34 @@ public final class Accounts {
return server;
});
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
*/
private static void removeDanglingAuthlibInjectorAccounts() {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
public static String getAccountTypeName(AccountFactory<?> factory) {
return Optional.ofNullable(loginType2name.get(factory))
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
}
public static String getAccountTypeName(Account account) {
return getAccountTypeName(getAccountFactory(account));
}
// ====
}

View File

@@ -129,7 +129,7 @@ public final class Config implements Cloneable, Observable {
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty("");
@@ -379,8 +379,8 @@ public final class Config implements Cloneable, Observable {
return configurations;
}
public ObservableList<Map<Object, Object>> getAccounts() {
return accounts;
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
public String getSelectedAccount() {

View File

@@ -36,7 +36,7 @@ public final class ProxyManager {
private ProxyManager() {
}
private static final ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
private static ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
() -> {
String host = CONFIG.getProxyHost();
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
@@ -59,11 +59,7 @@ public final class ProxyManager {
return proxyProperty;
}
static {
initProxy();
}
private static void initProxy() {
static void init() {
proxyProperty.addListener(observable -> updateSystemProxy());
updateSystemProxy();

View File

@@ -17,16 +17,10 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
@@ -35,82 +29,37 @@ import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class Settings {
public static final Settings INSTANCE = new Settings();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final boolean firstLaunch;
private InvalidationListener accountChangeListener =
source -> CONFIG.getAccounts().setAll(
accounts.values().stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
return storage;
})
.collect(toList()));
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
ProxyManager.getProxy(); // init ProxyManager
for (Iterator<Map<Object, Object>> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
if (factory == null) {
LOG.warning("Unrecognized account type, removing: " + settings);
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings);
} catch (Exception e) {
LOG.log(Level.WARNING, "Malformed account storage, removing: " + settings, e);
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
}
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
this.selectedAccount.set(accounts.get(CONFIG.getSelectedAccount()));
ProxyManager.init();
Accounts.init();
checkProfileMap();
save();
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> save());
}
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
CONFIG.addListener(source -> save());
}
private void save() {
LOG.info("Saving config");
ConfigHolder.saveConfig(CONFIG);
}
@@ -167,23 +116,6 @@ public class Settings {
}
}
/****************************************
* AUTHLIB INJECTORS *
****************************************/
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
* Don't call this before {@link #migrateAuthlibInjectorServers()} is called, otherwise old data would be lost.
*/
private void removeDanglingAuthlibInjectorAccounts() {
accounts.values().stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(((AuthlibInjectorAccount) it).getServer()))
.collect(toList())
.forEach(this::deleteAccount);
}
/****************************************
* DOWNLOAD PROVIDERS *
****************************************/
@@ -199,86 +131,6 @@ public class Settings {
CONFIG.setDownloadType(index);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
if (a == null || !accounts.containsKey(Accounts.getAccountId(a))) {
Account acc = accounts.values().stream().findAny().orElse(null);
set(acc);
return acc;
} else return a;
}
@Override
public void set(Account newValue) {
if (newValue == null || accounts.containsKey(Accounts.getAccountId(newValue))) {
super.set(newValue);
}
}
@Override
public void invalidated() {
super.invalidated();
CONFIG.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
public Account getSelectedAccount() {
return selectedAccount.get();
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public void setSelectedAccount(Account selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
return accounts.get(Accounts.getAccountId(name, character));
}
public Collection<Account> getAccounts() {
return Collections.unmodifiableCollection(accounts.values());
}
public void deleteAccount(String name, String character) {
Account removed = accounts.remove(Accounts.getAccountId(name, character));
if (removed != null) {
removed.removeListener(accountChangeListener);
accountChangeListener.invalidated(removed);
onAccountLoading();
selectedAccount.get();
}
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
account.removeListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
selectedAccount.get();
}
/****************************************
* PROFILES *
****************************************/
@@ -369,8 +221,4 @@ public class Settings {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
}

View File

@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
@@ -97,7 +97,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblType.setText(Accounts.getAccountTypeName(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
@@ -105,7 +105,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Accounts.getAccounts().remove(account);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}

View File

@@ -21,7 +21,7 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
@@ -33,15 +33,12 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
@@ -51,24 +48,25 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@@ -85,23 +83,31 @@ public class AddAccountPane extends StackPane {
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector"));
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
cboType.getSelectionModel().select(0);
ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginTypeIdProperty.isEqualTo(2));
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()),
txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty));
() -> !( // consider the opposite situation: input is valid
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(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
}
/**
@@ -113,47 +119,48 @@ public class AddAccountPane extends StackPane {
}
}
/**
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
*/
private Object getAuthAdditionalData() {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
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;
String username = txtUsername.getText();
String password = txtPassword.getText();
Object addtionalData;
int type = cboType.getSelectionModel().getSelectedIndex();
AccountFactory<?> factory;
switch (type) {
case 0:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY);
addtionalData = null;
break;
case 1:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY);
addtionalData = null;
break;
case 2:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY);
Optional<AuthlibInjectorServer> server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem());
if (server.isPresent()) {
addtionalData = server.get();
} else {
lblCreationWarning.setText(i18n("account.failed.no_selected_server"));
return;
}
break;
default:
throw new Error();
}
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData))
String username = txtUsername.getText();
String password = txtPassword.getText();
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
.finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
Account account = variables.get("create_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 -> {
@@ -272,11 +279,4 @@ public class AddAccountPane extends StackPane {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return i18n("account.methods.yggdrasil");
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
}

View File

@@ -17,17 +17,18 @@
*/
package org.jackhuang.hmcl.ui;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
@@ -42,26 +43,21 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private InvalidationListener serversListener;
private ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serversListener = observable -> updateServersList();
CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener));
updateServersList();
serverItems = MappedObservableList.create(CONFIG.getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
}
private void updateServersList() {
listPane.getChildren().setAll(
CONFIG.getAuthlibInjectorServers().stream()
.map(server -> new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())))
.collect(toList()));
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
Controllers.dialog(new AddAuthlibInjectorServerPane());

View File

@@ -21,6 +21,12 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
@@ -49,21 +55,39 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.util.MappedObservableList;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static javafx.collections.FXCollections.singletonObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
private ListProperty<RipplerContainer> accountItems = new SimpleListProperty<>();
private ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accountItems.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Account selected = get();
accountItems.forEach(item -> item.setSelected(
getAccountFromItem(item)
.map(it -> it == selected)
.orElse(false)));
}
};
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
@@ -90,30 +114,91 @@ public final class LeftPaneController {
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
// ==== Accounts ====
// Missing account item
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
accountItems.bind(
new When(Accounts.accountsProperty().emptyProperty())
.then(singletonObservableList(missingAccountRippler))
.otherwise(MappedObservableList.create(Accounts.getAccounts(), this::createAccountItem)));
Bindings.bindContent(accountPane.getChildren(), accountItems);
selectedAccount.bindBidirectional(Accounts.selectedAccountProperty());
// ====
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
}
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
// ==== Accounts ====
private Optional<Account> getAccountFromItem(RipplerContainer accountItem) {
return Optional.ofNullable(accountItem.getProperties().get("account"))
.map(Account.class::cast);
}
private static String accountSubtitle(Account account) {
if (account instanceof OfflineAccount)
return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount)
return account.getUsername();
else
return "";
}
private RipplerContainer createAccountItem(Account account) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
RipplerContainer rippler = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
rippler.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
selectedAccount.set(account);
}
});
rippler.getProperties().put("account", account);
rippler.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else {
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
}
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(rippler, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
// update skin
if (account instanceof YggdrasilAccount) {
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
return rippler;
}
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
addNewAccount();
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
// ====
private void onProfileChanged(ProfileChangedEvent event) {
Profile profile = event.getProfile();
@@ -143,70 +228,6 @@ public final class LeftPaneController {
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
Settings.INSTANCE.setSelectedAccount(account);
});
ripplerContainer.getProperties().put("account", account);
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
@@ -247,9 +268,4 @@ public final class LeftPaneController {
}
});
}
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
}

View File

@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
@@ -132,13 +133,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
@@ -150,7 +151,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
@@ -214,10 +215,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
}
});
File iconFile = repository.getVersionIcon(id);

View File

@@ -104,7 +104,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private Pane proxyPane;
{
private ObjectProperty<Proxy.Type> selectedProxyType;
public SettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
FXUtils.smoothScrolling(scroll);
@@ -151,7 +153,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
ObjectProperty<Proxy.Type> selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@@ -196,7 +198,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
// background
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
), EnumBackgroundImage.CUSTOM);
@@ -206,8 +208,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(CONFIG.backgroundImageProperty()));
// ====
// theme
// ==== Theme ====
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
@@ -219,6 +222,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
// ====
}
public String getTitle() {

View File

@@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -67,7 +67,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
txtModpackName.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse(""));
txtModpackAuthor.setText(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();

View File

@@ -39,7 +39,6 @@ account.failed.invalid_credentials=Incorrect password, or you are forbidden to l
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.injector.add=Add an authentication server
account.injector.manage=Manage authentication servers
@@ -50,7 +49,6 @@ account.injector.server_url=Server URL
account.injector.server_name=Server Name
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.no_method=No login method
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=None

View File

@@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
@@ -50,7 +49,6 @@ account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.methods.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶

View File

@@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
@@ -50,7 +49,6 @@ account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
account.methods.no_method=没有登入方式
account.methods.offline=离线模式
account.methods.yggdrasil=正版登录
account.missing=没有账户

View File

@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
@@ -110,4 +111,16 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return server;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), server.hashCode());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AuthlibInjectorAccount))
return false;
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
return super.equals(another) && server.equals(another.server);
}
}

View File

@@ -105,4 +105,17 @@ public class OfflineAccount extends Account {
.append("uuid", uuid)
.toString();
}
@Override
public int hashCode() {
return username.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OfflineAccount))
return false;
OfflineAccount another = (OfflineAccount) obj;
return username.equals(another.username);
}
}

View File

@@ -169,4 +169,16 @@ public class YggdrasilAccount extends Account {
return "YggdrasilAccount[username=" + getUsername() + "]";
}
@Override
public int hashCode() {
return characterUUID.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof YggdrasilAccount))
return false;
YggdrasilAccount another = (YggdrasilAccount) obj;
return characterUUID.equals(another.characterUUID);
}
}

View File

@@ -41,7 +41,7 @@ public final class Lang {
*/
@SafeVarargs
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
HashMap<K, V> map = new HashMap<>();
Map<K, V> map = new LinkedHashMap<>();
for (Pair<K, V> pair : pairs)
map.put(pair.getKey(), pair.getValue());
return map;

View File

@@ -0,0 +1,160 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import static java.util.stream.Collectors.toCollection;
import static javafx.collections.FXCollections.unmodifiableObservableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
/**
* @author yushijinhun
*/
public final class MappedObservableList {
private MappedObservableList() {
}
private static class ReferenceHolder implements InvalidationListener {
@SuppressWarnings("unused")
private Object ref;
ReferenceHolder(Object ref) {
this.ref = ref;
}
@Override
public void invalidated(Observable observable) {
// no-op
}
}
private static class MappedObservableListUpdater<T, U> implements ListChangeListener<T> {
private ObservableList<T> origin;
private ObservableList<U> target;
private Function<T, U> mapper;
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
private List<U> buffer;
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
this.origin = origin;
this.target = target;
this.mapper = mapper;
this.buffer = new ArrayList<>(target);
}
@Override
public void onChanged(Change<? extends T> change) {
// cache removed elements to reduce calls to mapper
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
if (change.wasPermutated()) {
@SuppressWarnings("unchecked")
U[] temp = (U[]) new Object[to - from];
for (int i = 0; i < temp.length; i++) {
temp[i] = buffer.get(from + i);
}
for (int idx = from; idx < to; idx++) {
buffer.set(change.getPermutation(idx), temp[idx - from]);
}
} else {
if (change.wasRemoved()) {
List<? extends T> originRemoved = change.getRemoved();
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
for (int i = 0; i < targetRemoved.size(); i++) {
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
}
targetRemoved.clear();
}
if (change.wasAdded()) {
@SuppressWarnings("unchecked")
U[] toAdd = (U[]) new Object[to - from];
for (int i = 0; i < toAdd.length; i++) {
toAdd[i] = map(cache, origin.get(from + i));
}
buffer.addAll(from, Arrays.asList(toAdd));
}
}
}
target.setAll(buffer);
}
private void pushCache(Map<T, LinkedList<U>> cache, T key, U value) {
cache.computeIfAbsent(key, any -> new LinkedList<>())
.push(value);
}
private U map(Map<T, LinkedList<U>> cache, T key) {
LinkedList<U> stack = cache.get(key);
if (stack != null && !stack.isEmpty()) {
return stack.pop();
}
return mapper.apply(key);
}
}
/**
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
*
* If an item is added to {@code origin}, {@code mapper} will be invoked to create a corresponding item, which will also be added to the returned {@code ObservableList}.
* If an item is removed from {@code origin}, the corresponding item in the returned {@code ObservableList} will also be removed.
* If {@code origin} is permutated, the returned {@code ObservableList} will also be permutated in the same way.
*
* The returned {@code ObservableList} is unmodifiable.
*/
public static <T, U> ObservableList<U> create(ObservableList<T> origin, Function<T, U> mapper) {
// create a already-synchronized target ObservableList<U>
ObservableList<U> target = origin.stream()
.map(mapper)
.collect(toCollection(FXCollections::observableArrayList));
// then synchronize further changes to target
ListChangeListener<T> listener = new MappedObservableListUpdater<>(origin, target, mapper);
// let target hold a reference to listener to prevent listener being garbage-collected before target is garbage-collected
target.addListener(new ReferenceHolder(listener));
// let origin hold a weak reference to listener, so that target can be garbage-collected when it's no longer used
origin.addListener(new WeakListChangeListener<>(listener));
// ref graph:
// target ------> listener <-weak- origin
// <------ ------>
return unmodifiableObservableList(target);
}
}