@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.auth.Account;
|
|||||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
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.FileDownloadTask;
|
||||||
import org.jackhuang.hmcl.task.Scheduler;
|
import org.jackhuang.hmcl.task.Scheduler;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
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 final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
|
||||||
|
|
||||||
public static void loadSkins() {
|
public static void loadSkins() {
|
||||||
for (Account account : Settings.INSTANCE.getAccounts()) {
|
for (Account account : Accounts.getAccounts()) {
|
||||||
if (account instanceof YggdrasilAccount) {
|
if (account instanceof YggdrasilAccount) {
|
||||||
new SkinLoadTask((YggdrasilAccount) account, false).start();
|
new SkinLoadTask((YggdrasilAccount) account, false).start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* Hello Minecraft! Launcher.
|
* Hello Minecraft! Launcher.
|
||||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* 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.MojangYggdrasilProvider;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
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.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.logging.Level;
|
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.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.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
@@ -44,33 +57,160 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
|||||||
public final class Accounts {
|
public final class Accounts {
|
||||||
private Accounts() {}
|
private Accounts() {}
|
||||||
|
|
||||||
public static final String OFFLINE_ACCOUNT_KEY = "offline";
|
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||||
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
|
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
|
||||||
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
|
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(
|
private static final String TYPE_OFFLINE = "offline";
|
||||||
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
|
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
|
||||||
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
|
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
|
||||||
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(
|
|
||||||
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
|
|
||||||
Accounts::getOrCreateAuthlibInjectorServer))
|
|
||||||
);
|
|
||||||
|
|
||||||
public static String getAccountType(Account account) {
|
private static Map<String, AccountFactory<?>> type2factory = mapOf(
|
||||||
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
|
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
|
||||||
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
|
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
|
||||||
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
|
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
|
||||||
else return YGGDRASIL_ACCOUNT_KEY;
|
|
||||||
|
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) {
|
public static AccountFactory<?> getAccountFactory(Account account) {
|
||||||
return getAccountId(account.getUsername(), account.getCharacter());
|
return type2factory.get(accountType(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getAccountId(String username, String character) {
|
private static String accountId(Account account) {
|
||||||
return username + ":" + character;
|
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) {
|
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||||
return CONFIG.getAuthlibInjectorServers().stream()
|
return CONFIG.getAuthlibInjectorServers().stream()
|
||||||
.filter(server -> url.equals(server.getUrl()))
|
.filter(server -> url.equals(server.getUrl()))
|
||||||
@@ -90,4 +230,34 @@ public final class Accounts {
|
|||||||
return server;
|
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));
|
||||||
|
}
|
||||||
|
// ====
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ public final class Config implements Cloneable, Observable {
|
|||||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
||||||
|
|
||||||
@SerializedName("accounts")
|
@SerializedName("accounts")
|
||||||
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
|
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
@SerializedName("selectedAccount")
|
@SerializedName("selectedAccount")
|
||||||
private StringProperty selectedAccount = new SimpleStringProperty("");
|
private StringProperty selectedAccount = new SimpleStringProperty("");
|
||||||
@@ -379,8 +379,8 @@ public final class Config implements Cloneable, Observable {
|
|||||||
return configurations;
|
return configurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableList<Map<Object, Object>> getAccounts() {
|
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||||
return accounts;
|
return accountStorages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSelectedAccount() {
|
public String getSelectedAccount() {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public final class ProxyManager {
|
|||||||
private ProxyManager() {
|
private ProxyManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
|
private static ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
|
||||||
() -> {
|
() -> {
|
||||||
String host = CONFIG.getProxyHost();
|
String host = CONFIG.getProxyHost();
|
||||||
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
|
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
|
||||||
@@ -59,11 +59,7 @@ public final class ProxyManager {
|
|||||||
return proxyProperty;
|
return proxyProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static void init() {
|
||||||
initProxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void initProxy() {
|
|
||||||
proxyProperty.addListener(observable -> updateSystemProxy());
|
proxyProperty.addListener(observable -> updateSystemProxy());
|
||||||
|
|
||||||
updateSystemProxy();
|
updateSystemProxy();
|
||||||
|
|||||||
@@ -17,16 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.InvalidationListener;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Launcher;
|
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.download.DownloadProvider;
|
||||||
import org.jackhuang.hmcl.event.*;
|
import org.jackhuang.hmcl.event.*;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -35,82 +29,37 @@ import org.jackhuang.hmcl.util.i18n.Locales;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.stream.Collectors;
|
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.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;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
|
|
||||||
public static final Settings INSTANCE = new Settings();
|
public static final Settings INSTANCE = new Settings();
|
||||||
|
|
||||||
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private final boolean firstLaunch;
|
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() {
|
private Settings() {
|
||||||
firstLaunch = CONFIG.isFirstLaunch();
|
firstLaunch = CONFIG.isFirstLaunch();
|
||||||
CONFIG.setFirstLaunch(false);
|
CONFIG.setFirstLaunch(false);
|
||||||
|
|
||||||
ProxyManager.getProxy(); // init ProxyManager
|
ProxyManager.init();
|
||||||
|
Accounts.init();
|
||||||
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()));
|
|
||||||
|
|
||||||
checkProfileMap();
|
checkProfileMap();
|
||||||
|
|
||||||
save();
|
|
||||||
|
|
||||||
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
|
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
|
||||||
profileEntry.getValue().setName(profileEntry.getKey());
|
profileEntry.getValue().setName(profileEntry.getKey());
|
||||||
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
|
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
|
||||||
profileEntry.getValue().addPropertyChangedListener(e -> save());
|
profileEntry.getValue().addPropertyChangedListener(e -> save());
|
||||||
}
|
}
|
||||||
|
|
||||||
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
|
|
||||||
|
|
||||||
CONFIG.addListener(source -> save());
|
CONFIG.addListener(source -> save());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void save() {
|
private void save() {
|
||||||
|
LOG.info("Saving config");
|
||||||
ConfigHolder.saveConfig(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 *
|
* DOWNLOAD PROVIDERS *
|
||||||
****************************************/
|
****************************************/
|
||||||
@@ -199,86 +131,6 @@ public class Settings {
|
|||||||
CONFIG.setDownloadType(index);
|
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 *
|
* PROFILES *
|
||||||
****************************************/
|
****************************************/
|
||||||
@@ -369,8 +221,4 @@ public class Settings {
|
|||||||
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
|
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
|
||||||
onProfileChanged();
|
onProfileChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAccountLoading() {
|
|
||||||
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
|||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||||
import org.jackhuang.hmcl.game.AccountHelper;
|
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.setting.Theme;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
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));
|
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
|
||||||
|
|
||||||
lblCharacter.setText(account.getCharacter());
|
lblCharacter.setText(account.getCharacter());
|
||||||
lblType.setText(AddAccountPane.accountType(account));
|
lblType.setText(Accounts.getAccountTypeName(account));
|
||||||
lblEmail.setText(account.getUsername());
|
lblEmail.setText(account.getUsername());
|
||||||
|
|
||||||
btnRefresh.setVisible(account instanceof YggdrasilAccount);
|
btnRefresh.setVisible(account instanceof YggdrasilAccount);
|
||||||
@@ -105,7 +105,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void onDelete() {
|
private void onDelete() {
|
||||||
Settings.INSTANCE.deleteAccount(account);
|
Accounts.getAccounts().remove(account);
|
||||||
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
|
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import com.jfoenix.concurrency.JFXUtilities;
|
|||||||
import com.jfoenix.controls.*;
|
import com.jfoenix.controls.*;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.Hyperlink;
|
||||||
@@ -33,15 +33,12 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.auth.*;
|
import org.jackhuang.hmcl.auth.*;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
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.GameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||||
import org.jackhuang.hmcl.game.AccountHelper;
|
import org.jackhuang.hmcl.game.AccountHelper;
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.setting.Settings;
|
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
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.ui.construct.Validator;
|
||||||
import org.jackhuang.hmcl.util.Constants;
|
import org.jackhuang.hmcl.util.Constants;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
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.setting.ConfigHolder.CONFIG;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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 {
|
public class AddAccountPane extends StackPane {
|
||||||
|
|
||||||
@FXML private JFXTextField txtUsername;
|
@FXML private JFXTextField txtUsername;
|
||||||
@FXML private JFXPasswordField txtPassword;
|
@FXML private JFXPasswordField txtPassword;
|
||||||
@FXML private Label lblCreationWarning;
|
@FXML private Label lblCreationWarning;
|
||||||
@FXML private Label lblPassword;
|
@FXML private Label lblPassword;
|
||||||
@FXML private JFXComboBox<String> cboType;
|
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||||
@FXML private Label lblInjectorServer;
|
@FXML private Label lblInjectorServer;
|
||||||
@FXML private Hyperlink linkManageInjectorServers;
|
@FXML private Hyperlink linkManageInjectorServers;
|
||||||
@@ -85,23 +83,31 @@ public class AddAccountPane extends StackPane {
|
|||||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||||
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);
|
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());
|
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());
|
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||||
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
|
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
|
||||||
|
|
||||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
|
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
|
||||||
|
|
||||||
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
||||||
() -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()),
|
() -> !( // consider the opposite situation: input is valid
|
||||||
txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty));
|
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
|
@FXML
|
||||||
private void onCreationAccept() {
|
private void onCreationAccept() {
|
||||||
if (btnAccept.isDisabled())
|
if (btnAccept.isDisabled())
|
||||||
return;
|
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();
|
acceptPane.showSpinner();
|
||||||
lblCreationWarning.setText("");
|
lblCreationWarning.setText("");
|
||||||
setDisable(true);
|
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 -> {
|
.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();
|
acceptPane.hideSpinner();
|
||||||
fireEvent(new DialogCloseEvent());
|
fireEvent(new DialogCloseEvent());
|
||||||
}, exception -> {
|
}, exception -> {
|
||||||
@@ -272,11 +279,4 @@ public class AddAccountPane extends StackPane {
|
|||||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,18 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui;
|
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.loadFXML;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
|
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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.ui.wizard.DecoratorPage;
|
||||||
|
import org.jackhuang.hmcl.util.MappedObservableList;
|
||||||
|
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.WeakInvalidationListener;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
@@ -42,26 +43,21 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
|
|||||||
@FXML private VBox listPane;
|
@FXML private VBox listPane;
|
||||||
@FXML private StackPane contentPane;
|
@FXML private StackPane contentPane;
|
||||||
|
|
||||||
private InvalidationListener serversListener;
|
private ObservableList<AuthlibInjectorServerItem> serverItems;
|
||||||
|
|
||||||
public AuthlibInjectorServersPage() {
|
public AuthlibInjectorServersPage() {
|
||||||
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
|
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
|
||||||
smoothScrolling(scrollPane);
|
smoothScrolling(scrollPane);
|
||||||
|
|
||||||
serversListener = observable -> updateServersList();
|
serverItems = MappedObservableList.create(CONFIG.getAuthlibInjectorServers(), this::createServerItem);
|
||||||
CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener));
|
Bindings.bindContent(listPane.getChildren(), serverItems);
|
||||||
updateServersList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateServersList() {
|
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
|
||||||
listPane.getChildren().setAll(
|
return new AuthlibInjectorServerItem(server,
|
||||||
CONFIG.getAuthlibInjectorServers().stream()
|
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer()));
|
||||||
.map(server -> new AuthlibInjectorServerItem(server,
|
|
||||||
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())))
|
|
||||||
.collect(toList()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void onAdd() {
|
private void onAdd() {
|
||||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import com.jfoenix.concurrency.JFXUtilities;
|
|||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXPopup;
|
import com.jfoenix.controls.JFXPopup;
|
||||||
import javafx.application.Platform;
|
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.Node;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.image.Image;
|
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.IconedItem;
|
||||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
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.io.File;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
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 {
|
public final class LeftPaneController {
|
||||||
private final AdvancedListBox leftPane;
|
private final AdvancedListBox leftPane;
|
||||||
private final VBox profilePane = new VBox();
|
private final VBox profilePane = new VBox();
|
||||||
private final VBox accountPane = new VBox();
|
private final VBox accountPane = new VBox();
|
||||||
private final IconedItem launcherSettingsItem;
|
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) {
|
public LeftPaneController(AdvancedListBox leftPane) {
|
||||||
this.leftPane = leftPane;
|
this.leftPane = leftPane;
|
||||||
@@ -90,30 +114,91 @@ public final class LeftPaneController {
|
|||||||
})))
|
})))
|
||||||
.add(profilePane);
|
.add(profilePane);
|
||||||
|
|
||||||
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
|
// ==== Accounts ====
|
||||||
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
|
// 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(ProfileLoadingEvent.class).register(this::onProfilesLoading);
|
||||||
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
|
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
|
||||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
|
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
|
||||||
|
}
|
||||||
|
|
||||||
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
|
// ==== Accounts ====
|
||||||
onAccountsLoading();
|
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() {
|
private void addNewAccount() {
|
||||||
Controllers.dialog(new AddAccountPane());
|
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) {
|
private void onProfileChanged(ProfileChangedEvent event) {
|
||||||
Profile profile = event.getProfile();
|
Profile profile = event.getProfile();
|
||||||
@@ -143,70 +228,6 @@ public final class LeftPaneController {
|
|||||||
Platform.runLater(() -> profilePane.getChildren().setAll(list));
|
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() {
|
public void showUpdate() {
|
||||||
launcherSettingsItem.setText(i18n("update.found"));
|
launcherSettingsItem.setText(i18n("update.found"));
|
||||||
launcherSettingsItem.setTextFill(Color.RED);
|
launcherSettingsItem.setTextFill(Color.RED);
|
||||||
@@ -247,9 +268,4 @@ public final class LeftPaneController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkAccount() {
|
|
||||||
if (Settings.INSTANCE.getAccounts().isEmpty())
|
|
||||||
addNewAccount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
|||||||
import org.jackhuang.hmcl.game.*;
|
import org.jackhuang.hmcl.game.*;
|
||||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||||
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.setting.Settings;
|
import org.jackhuang.hmcl.setting.Settings;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -132,13 +133,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
|||||||
});
|
});
|
||||||
item.setVersionName(id);
|
item.setVersionName(id);
|
||||||
item.setOnLaunchButtonClicked(e -> {
|
item.setOnLaunchButtonClicked(e -> {
|
||||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
if (Accounts.getSelectedAccount() == null)
|
||||||
Controllers.getLeftPaneController().checkAccount();
|
Controllers.getLeftPaneController().checkAccount();
|
||||||
else
|
else
|
||||||
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
|
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
|
||||||
});
|
});
|
||||||
item.setOnScriptButtonClicked(e -> {
|
item.setOnScriptButtonClicked(e -> {
|
||||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
if (Accounts.getSelectedAccount() == null)
|
||||||
Controllers.dialog(i18n("login.empty_username"));
|
Controllers.dialog(i18n("login.empty_username"));
|
||||||
else {
|
else {
|
||||||
FileChooser chooser = new FileChooser();
|
FileChooser chooser = new FileChooser();
|
||||||
@@ -150,7 +151,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
|||||||
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
|
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
|
||||||
File file = chooser.showSaveDialog(Controllers.getStage());
|
File file = chooser.showSaveDialog(Controllers.getStage());
|
||||||
if (file != null)
|
if (file != null)
|
||||||
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
|
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
item.setOnSettingsButtonClicked(e -> {
|
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());
|
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||||
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
if (Accounts.getSelectedAccount() == null)
|
||||||
Controllers.dialog(i18n("login.empty_username"));
|
Controllers.dialog(i18n("login.empty_username"));
|
||||||
else
|
else
|
||||||
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
|
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
File iconFile = repository.getVersionIcon(id);
|
File iconFile = repository.getVersionIcon(id);
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
|||||||
@FXML
|
@FXML
|
||||||
private Pane proxyPane;
|
private Pane proxyPane;
|
||||||
|
|
||||||
{
|
private ObjectProperty<Proxy.Type> selectedProxyType;
|
||||||
|
|
||||||
|
public SettingsPage() {
|
||||||
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
|
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
|
||||||
|
|
||||||
FXUtils.smoothScrolling(scroll);
|
FXUtils.smoothScrolling(scroll);
|
||||||
@@ -151,7 +153,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
|||||||
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
|
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
|
||||||
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
|
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();
|
invalidated();
|
||||||
}
|
}
|
||||||
@@ -196,7 +198,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
|||||||
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
||||||
checkUpdate();
|
checkUpdate();
|
||||||
|
|
||||||
// background
|
// ==== Background ====
|
||||||
backgroundItem.loadChildren(Collections.singletonList(
|
backgroundItem.loadChildren(Collections.singletonList(
|
||||||
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
|
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
|
||||||
), EnumBackgroundImage.CUSTOM);
|
), EnumBackgroundImage.CUSTOM);
|
||||||
@@ -206,8 +208,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
|||||||
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
|
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
|
||||||
.then(i18n("launcher.background.default"))
|
.then(i18n("launcher.background.default"))
|
||||||
.otherwise(CONFIG.backgroundImageProperty()));
|
.otherwise(CONFIG.backgroundImageProperty()));
|
||||||
|
// ====
|
||||||
|
|
||||||
// theme
|
// ==== Theme ====
|
||||||
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
|
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
|
||||||
picker.setCustomColorText(i18n("color.custom"));
|
picker.setCustomColorText(i18n("color.custom"));
|
||||||
picker.setRecentColorsText(i18n("color.recent"));
|
picker.setRecentColorsText(i18n("color.recent"));
|
||||||
@@ -219,6 +222,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
|||||||
});
|
});
|
||||||
themeColorPickerContainer.getChildren().setAll(picker);
|
themeColorPickerContainer.getChildren().setAll(picker);
|
||||||
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
|
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
|
||||||
|
// ====
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.Launcher;
|
import org.jackhuang.hmcl.Launcher;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
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.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||||
@@ -67,7 +67,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
|
|||||||
txtModpackName.textProperty().addListener(e -> checkValidation());
|
txtModpackName.textProperty().addListener(e -> checkValidation());
|
||||||
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
|
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
|
||||||
txtModpackVersion.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);
|
lblVersionName.setText(version);
|
||||||
|
|
||||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||||
|
|||||||
@@ -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_password=Invalid password
|
||||||
account.failed.invalid_token=Please log out and re-input your password to login.
|
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_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.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.add=Add an authentication server
|
||||||
account.injector.manage=Manage authentication servers
|
account.injector.manage=Manage authentication servers
|
||||||
@@ -50,7 +49,6 @@ account.injector.server_url=Server URL
|
|||||||
account.injector.server_name=Server Name
|
account.injector.server_name=Server Name
|
||||||
account.methods=Login Type
|
account.methods=Login Type
|
||||||
account.methods.authlib_injector=authlib-injector
|
account.methods.authlib_injector=authlib-injector
|
||||||
account.methods.no_method=No login method
|
|
||||||
account.methods.offline=Offline
|
account.methods.offline=Offline
|
||||||
account.methods.yggdrasil=Mojang
|
account.methods.yggdrasil=Mojang
|
||||||
account.missing=None
|
account.missing=None
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄
|
|||||||
account.failed.invalid_password=無效的密碼
|
account.failed.invalid_password=無效的密碼
|
||||||
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
|
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
|
||||||
account.failed.no_character=該帳號沒有角色
|
account.failed.no_character=該帳號沒有角色
|
||||||
account.failed.no_selected_server=未選擇認證服務器
|
|
||||||
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
|
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
|
||||||
account.injector.add=添加認證服務器
|
account.injector.add=添加認證服務器
|
||||||
account.injector.manage=管理認證服務器
|
account.injector.manage=管理認證服務器
|
||||||
@@ -50,7 +49,6 @@ account.injector.server_url=服務器地址
|
|||||||
account.injector.server_name=服務器名稱
|
account.injector.server_name=服務器名稱
|
||||||
account.methods=登錄方式
|
account.methods=登錄方式
|
||||||
account.methods.authlib_injector=authlib-injector 登錄
|
account.methods.authlib_injector=authlib-injector 登錄
|
||||||
account.methods.no_method=沒有登入方式
|
|
||||||
account.methods.offline=離線模式
|
account.methods.offline=離線模式
|
||||||
account.methods.yggdrasil=正版登錄
|
account.methods.yggdrasil=正版登錄
|
||||||
account.missing=沒有賬戶
|
account.missing=沒有賬戶
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录
|
|||||||
account.failed.invalid_password=无效的密码
|
account.failed.invalid_password=无效的密码
|
||||||
account.failed.invalid_token=请尝试登出并重新输入密码登录
|
account.failed.invalid_token=请尝试登出并重新输入密码登录
|
||||||
account.failed.no_character=该帐号没有角色
|
account.failed.no_character=该帐号没有角色
|
||||||
account.failed.no_selected_server=未选择认证服务器
|
|
||||||
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
||||||
account.injector.add=添加认证服务器
|
account.injector.add=添加认证服务器
|
||||||
account.injector.manage=管理认证服务器
|
account.injector.manage=管理认证服务器
|
||||||
@@ -50,7 +49,6 @@ account.injector.server_url=服务器地址
|
|||||||
account.injector.server_name=服务器名称
|
account.injector.server_name=服务器名称
|
||||||
account.methods=登录方式
|
account.methods=登录方式
|
||||||
account.methods.authlib_injector=authlib-injector 登录
|
account.methods.authlib_injector=authlib-injector 登录
|
||||||
account.methods.no_method=没有登入方式
|
|
||||||
account.methods.offline=离线模式
|
account.methods.offline=离线模式
|
||||||
account.methods.yggdrasil=正版登录
|
account.methods.yggdrasil=正版登录
|
||||||
account.missing=没有账户
|
account.missing=没有账户
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.NetworkUtils;
|
|||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -110,4 +111,16 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
return server;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,4 +105,17 @@ public class OfflineAccount extends Account {
|
|||||||
.append("uuid", uuid)
|
.append("uuid", uuid)
|
||||||
.toString();
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,4 +169,16 @@ public class YggdrasilAccount extends Account {
|
|||||||
return "YggdrasilAccount[username=" + getUsername() + "]";
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public final class Lang {
|
|||||||
*/
|
*/
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
|
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)
|
for (Pair<K, V> pair : pairs)
|
||||||
map.put(pair.getKey(), pair.getValue());
|
map.put(pair.getKey(), pair.getValue());
|
||||||
return map;
|
return map;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user