mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2024-12-15 06:50:12 +08:00
* Save accounts in hmcl dir * close #1377: Add conversion button * fix YggdrasilAccount::getIdentifier() * Check whether the account exists before moving * Add server url to selected account * Add global prefix
This commit is contained in:
parent
4c2d1063ec
commit
d032b3c677
@ -17,11 +17,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyListProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyListWrapper;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.auth.*;
|
import org.jackhuang.hmcl.auth.*;
|
||||||
@ -36,14 +37,17 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
|||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||||
import org.jackhuang.hmcl.game.OAuthServer;
|
import org.jackhuang.hmcl.game.OAuthServer;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||||
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
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 java.util.stream.Collectors.toList;
|
||||||
@ -130,59 +134,20 @@ public final class Accounts {
|
|||||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String GLOBAL_PREFIX = "$GLOBAL:";
|
||||||
|
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||||
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
|
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
|
||||||
|
|
||||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
updateAccountStorages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if {@link #init()} hasn't been called.
|
* True if {@link #init()} hasn't been called.
|
||||||
*/
|
*/
|
||||||
private static boolean initialized = false;
|
private static boolean initialized = false;
|
||||||
|
|
||||||
static {
|
|
||||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<Object, Object> getAccountStorage(Account account) {
|
private static Map<Object, Object> getAccountStorage(Account account) {
|
||||||
Map<Object, Object> storage = account.toStorage();
|
Map<Object, Object> storage = account.toStorage();
|
||||||
storage.put("type", getLoginType(getAccountFactory(account)));
|
storage.put("type", getLoginType(getAccountFactory(account)));
|
||||||
if (account == selectedAccount.get()) {
|
|
||||||
storage.put("selected", true);
|
|
||||||
}
|
|
||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +157,67 @@ public final class Accounts {
|
|||||||
if (!initialized)
|
if (!initialized)
|
||||||
return;
|
return;
|
||||||
// update storage
|
// update storage
|
||||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
|
||||||
|
ArrayList<Map<Object, Object>> global = new ArrayList<>();
|
||||||
|
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Account account : accounts) {
|
||||||
|
Map<Object, Object> storage = getAccountStorage(account);
|
||||||
|
if (account.isPortable())
|
||||||
|
portable.add(storage);
|
||||||
|
else
|
||||||
|
global.add(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.equals(globalAccountStorages))
|
||||||
|
globalAccountStorages.setAll(global);
|
||||||
|
if (!portable.equals(config().getAccountStorages()))
|
||||||
|
config().getAccountStorages().setAll(portable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void loadGlobalAccountStorages() {
|
||||||
|
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
|
||||||
|
if (Files.exists(globalAccountsFile)) {
|
||||||
|
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
|
||||||
|
globalAccountStorages.setAll((List<Map<Object, Object>>)
|
||||||
|
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
|
||||||
|
}.getType()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to load global accounts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
|
||||||
|
LOG.info("Saving global accounts");
|
||||||
|
synchronized (globalAccountsFile) {
|
||||||
|
try {
|
||||||
|
synchronized (globalAccountsFile) {
|
||||||
|
FileUtils.saveSafely(globalAccountsFile, json);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globalAccountStorages.addListener(onInvalidating(() ->
|
||||||
|
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Account parseAccount(Map<Object, Object> storage) {
|
||||||
|
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||||
|
if (factory == null) {
|
||||||
|
LOG.warning("Unrecognized account type: " + storage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return factory.fromStorage(storage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -202,38 +227,97 @@ public final class Accounts {
|
|||||||
if (initialized)
|
if (initialized)
|
||||||
throw new IllegalStateException("Already initialized");
|
throw new IllegalStateException("Already initialized");
|
||||||
|
|
||||||
// load accounts
|
loadGlobalAccountStorages();
|
||||||
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);
|
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
// load accounts
|
||||||
selectedAccount.set(account);
|
Account selected = null;
|
||||||
|
for (Map<Object, Object> storage : config().getAccountStorages()) {
|
||||||
|
Account account = parseAccount(storage);
|
||||||
|
if (account != null) {
|
||||||
|
account.setPortable(true);
|
||||||
|
accounts.add(account);
|
||||||
|
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||||
|
selected = account;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
for (Map<Object, Object> storage : globalAccountStorages) {
|
||||||
|
Account account = parseAccount(storage);
|
||||||
|
if (account != null) {
|
||||||
|
accounts.add(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String selectedAccountIdentifier = config().getSelectedAccount();
|
||||||
|
if (selected == null && selectedAccountIdentifier != null) {
|
||||||
|
boolean portable = true;
|
||||||
|
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
|
||||||
|
portable = false;
|
||||||
|
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Account account : accounts) {
|
||||||
|
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
|
||||||
|
if (portable == account.isPortable()) {
|
||||||
|
selected = account;
|
||||||
|
break;
|
||||||
|
} else if (selected == null) {
|
||||||
|
selected = account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected == null && !accounts.isEmpty()) {
|
||||||
|
selected = accounts.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAccount.set(selected);
|
||||||
|
|
||||||
|
InvalidationListener listener = o -> {
|
||||||
|
// this method 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 account = selectedAccount.get();
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
if (account == null) {
|
||||||
|
// valid
|
||||||
|
} else {
|
||||||
|
// the previously selected account is gone, we can only set it to null here
|
||||||
|
selectedAccount.set(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (accounts.contains(account)) {
|
||||||
|
// valid
|
||||||
|
} else {
|
||||||
|
// the previously selected account is gone
|
||||||
|
selectedAccount.set(accounts.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
selectedAccount.addListener(listener);
|
||||||
|
selectedAccount.addListener(onInvalidating(() -> {
|
||||||
|
Account account = selectedAccount.get();
|
||||||
|
if (account != null)
|
||||||
|
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
|
||||||
|
else
|
||||||
|
config().setSelectedAccount(null);
|
||||||
|
}));
|
||||||
|
accounts.addListener(listener);
|
||||||
|
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||||
|
|
||||||
Account selected = selectedAccount.get();
|
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
|
Account finalSelected = selected;
|
||||||
Schedulers.io().execute(() -> {
|
Schedulers.io().execute(() -> {
|
||||||
try {
|
try {
|
||||||
selected.logIn();
|
finalSelected.logIn();
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -267,10 +351,6 @@ public final class Accounts {
|
|||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadOnlyListProperty<Account> accountsProperty() {
|
|
||||||
return accountsWrapper.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Account getSelectedAccount() {
|
public static Account getSelectedAccount() {
|
||||||
return selectedAccount.get();
|
return selectedAccount.get();
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ public final class Config implements Cloneable, Observable {
|
|||||||
|
|
||||||
public static final int CURRENT_UI_VERSION = 0;
|
public static final int CURRENT_UI_VERSION = 0;
|
||||||
|
|
||||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||||
@ -142,6 +142,9 @@ public final class Config implements Cloneable, Observable {
|
|||||||
@SerializedName("configurations")
|
@SerializedName("configurations")
|
||||||
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
||||||
|
|
||||||
|
@SerializedName("selectedAccount")
|
||||||
|
private StringProperty selectedAccount = new SimpleStringProperty();
|
||||||
|
|
||||||
@SerializedName("accounts")
|
@SerializedName("accounts")
|
||||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
@ -479,6 +482,18 @@ public final class Config implements Cloneable, Observable {
|
|||||||
return configurations;
|
return configurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSelectedAccount() {
|
||||||
|
return selectedAccount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedAccount(String selectedAccount) {
|
||||||
|
this.selectedAccount.set(selectedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty selectedAccountProperty() {
|
||||||
|
return selectedAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||||
return accountStorages;
|
return accountStorages;
|
||||||
}
|
}
|
||||||
|
@ -108,22 +108,5 @@ final class ConfigUpgrader {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tryCast(rawJson.get("selectedAccount"), String.class)
|
|
||||||
.ifPresent(selected -> {
|
|
||||||
deserialized.getAccountStorages().stream()
|
|
||||||
.filter(storage -> {
|
|
||||||
Object type = storage.get("type");
|
|
||||||
if ("offline".equals(type)) {
|
|
||||||
return selected.equals(storage.get("username") + ":" + storage.get("username"));
|
|
||||||
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
|
|
||||||
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.findFirst()
|
|
||||||
.ifPresent(storage -> storage.put("selected", true));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,42 +21,23 @@ import com.google.gson.*;
|
|||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.collections.ObservableMap;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import org.hildan.fxgson.creators.ObservableListCreator;
|
|
||||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
|
||||||
import org.hildan.fxgson.creators.ObservableSetCreator;
|
|
||||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
|
||||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
|
||||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.net.Proxy;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||||
public class GlobalConfig implements Cloneable, Observable {
|
public class GlobalConfig implements Cloneable, Observable {
|
||||||
|
|
||||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
|
||||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
|
||||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
|
||||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
|
||||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
|
||||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
|
||||||
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
|
|
||||||
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
|
|
||||||
.setPrettyPrinting()
|
|
||||||
.create();
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||||
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||||
if (loaded == null) {
|
if (loaded == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -93,7 +74,7 @@ public class GlobalConfig implements Cloneable, Observable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toJson() {
|
public String toJson() {
|
||||||
return CONFIG_GSON.toJson(this);
|
return Config.CONFIG_GSON.toJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -89,7 +89,7 @@ public final class Controllers {
|
|||||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||||
AccountListPage accountListPage = new AccountListPage();
|
AccountListPage accountListPage = new AccountListPage();
|
||||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||||
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
|
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
|
||||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||||
return accountListPage;
|
return accountListPage;
|
||||||
});
|
});
|
||||||
|
@ -73,13 +73,14 @@ public class AccountListItem extends RadioButton {
|
|||||||
setUserData(account);
|
setUserData(account);
|
||||||
|
|
||||||
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||||
|
String portableSuffix = account.isPortable() ? ", " + i18n("account.portable") : "";
|
||||||
if (account instanceof AuthlibInjectorAccount) {
|
if (account instanceof AuthlibInjectorAccount) {
|
||||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||||
subtitle.bind(Bindings.concat(
|
subtitle.bind(Bindings.concat(
|
||||||
loginTypeName, ", ", i18n("account.injector.server"), ": ",
|
loginTypeName, ", ", i18n("account.injector.server"), ": ",
|
||||||
Bindings.createStringBinding(server::getName, server)));
|
Bindings.createStringBinding(server::getName, server), portableSuffix));
|
||||||
} else {
|
} else {
|
||||||
subtitle.set(loginTypeName);
|
subtitle.set(loginTypeName + portableSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
||||||
|
@ -28,6 +28,7 @@ import javafx.scene.control.Tooltip;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
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.game.TexturesLoader;
|
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||||
@ -90,6 +91,41 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
HBox right = new HBox();
|
HBox right = new HBox();
|
||||||
right.setAlignment(Pos.CENTER_RIGHT);
|
right.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
JFXButton btnMove = new JFXButton();
|
||||||
|
SpinnerPane spinnerMove = new SpinnerPane();
|
||||||
|
spinnerMove.getStyleClass().add("small-spinner-pane");
|
||||||
|
btnMove.setOnMouseClicked(e -> {
|
||||||
|
Account account = skinnable.getAccount();
|
||||||
|
Accounts.getAccounts().remove(account);
|
||||||
|
if (account.isPortable()) {
|
||||||
|
account.setPortable(false);
|
||||||
|
if (!Accounts.getAccounts().contains(account))
|
||||||
|
Accounts.getAccounts().add(account);
|
||||||
|
} else {
|
||||||
|
account.setPortable(true);
|
||||||
|
if (!Accounts.getAccounts().contains(account)) {
|
||||||
|
int idx = 0;
|
||||||
|
for (int i = Accounts.getAccounts().size() - 1; i >= 0; i--) {
|
||||||
|
if (Accounts.getAccounts().get(i).isPortable()) {
|
||||||
|
idx = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Accounts.getAccounts().add(idx, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
btnMove.getStyleClass().add("toggle-icon4");
|
||||||
|
if (skinnable.getAccount().isPortable()) {
|
||||||
|
btnMove.setGraphic(SVG.earth(Theme.blackFillBinding(), -1, -1));
|
||||||
|
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
|
||||||
|
} else {
|
||||||
|
btnMove.setGraphic(SVG.export(Theme.blackFillBinding(), -1, -1));
|
||||||
|
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
|
||||||
|
}
|
||||||
|
spinnerMove.setContent(btnMove);
|
||||||
|
right.getChildren().add(spinnerMove);
|
||||||
|
|
||||||
JFXButton btnRefresh = new JFXButton();
|
JFXButton btnRefresh = new JFXButton();
|
||||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||||
|
@ -138,8 +138,11 @@ The link down below will guide you to migrate your Mojang account to a Microsoft
|
|||||||
account.methods.yggdrasil.purchase=Buy Minecraft
|
account.methods.yggdrasil.purchase=Buy Minecraft
|
||||||
account.missing=No Accounts
|
account.missing=No Accounts
|
||||||
account.missing.add=Click here to add one.
|
account.missing.add=Click here to add one.
|
||||||
|
account.move_to_global=Convert to global account
|
||||||
|
account.move_to_portable=Convert to portable account
|
||||||
account.not_logged_in=Not Logged in
|
account.not_logged_in=Not Logged in
|
||||||
account.password=Password
|
account.password=Password
|
||||||
|
account.portable=Portable Account
|
||||||
account.skin=Skin
|
account.skin=Skin
|
||||||
account.skin.file=Skin File
|
account.skin.file=Skin File
|
||||||
account.skin.model=Model
|
account.skin.model=Model
|
||||||
|
@ -125,8 +125,11 @@ account.methods.yggdrasil.migration.hint=自 2022 年 3 月 10 日起,Mojang
|
|||||||
account.methods.yggdrasil.purchase=購買 Minecraft
|
account.methods.yggdrasil.purchase=購買 Minecraft
|
||||||
account.missing=沒有遊戲帳戶
|
account.missing=沒有遊戲帳戶
|
||||||
account.missing.add=按一下此處加入帳戶
|
account.missing.add=按一下此處加入帳戶
|
||||||
|
account.move_to_global=轉換為全域帳戶
|
||||||
|
account.move_to_portable=轉換為便攜帳戶
|
||||||
account.not_logged_in=未登入
|
account.not_logged_in=未登入
|
||||||
account.password=密碼
|
account.password=密碼
|
||||||
|
account.portable=便攜帳戶
|
||||||
account.skin=皮膚
|
account.skin=皮膚
|
||||||
account.skin.file=皮膚圖片檔案
|
account.skin.file=皮膚圖片檔案
|
||||||
account.skin.model=模型
|
account.skin.model=模型
|
||||||
|
@ -125,8 +125,11 @@ account.methods.yggdrasil.migration.hint=自 2022 年 3 月 10 日起,Mojang
|
|||||||
account.methods.yggdrasil.purchase=购买 Minecraft
|
account.methods.yggdrasil.purchase=购买 Minecraft
|
||||||
account.missing=没有游戏帐户
|
account.missing=没有游戏帐户
|
||||||
account.missing.add=点击此处添加帐户
|
account.missing.add=点击此处添加帐户
|
||||||
|
account.move_to_global=转换为全局账户
|
||||||
|
account.move_to_portable=转换为便携账户
|
||||||
account.not_logged_in=未登录
|
account.not_logged_in=未登录
|
||||||
account.password=密码
|
account.password=密码
|
||||||
|
account.portable=便携账户
|
||||||
account.skin=皮肤
|
account.skin=皮肤
|
||||||
account.skin.file=皮肤图片文件
|
account.skin.file=皮肤图片文件
|
||||||
account.skin.model=模型
|
account.skin.model=模型
|
||||||
|
@ -23,12 +23,15 @@ import javafx.beans.Observable;
|
|||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -71,7 +74,23 @@ public abstract class Account implements Observable {
|
|||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObservableHelper helper = new ObservableHelper(this);
|
private final BooleanProperty portable = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
public BooleanProperty portableProperty() {
|
||||||
|
return portable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPortable() {
|
||||||
|
return portable.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPortable(boolean value) {
|
||||||
|
this.portable.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String getIdentifier();
|
||||||
|
|
||||||
|
private final ObservableHelper helper = new ObservableHelper(this);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(InvalidationListener listener) {
|
public void addListener(InvalidationListener listener) {
|
||||||
@ -95,12 +114,29 @@ public abstract class Account implements Observable {
|
|||||||
return Bindings.createObjectBinding(Optional::empty);
|
return Bindings.createObjectBinding(Optional::empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(portable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (!(obj instanceof Account))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Account another = (Account) obj;
|
||||||
|
return isPortable() == another.isPortable();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this)
|
return new ToStringBuilder(this)
|
||||||
.append("username", getUsername())
|
.append("username", getUsername())
|
||||||
.append("character", getCharacter())
|
.append("character", getCharacter())
|
||||||
.append("uuid", getUUID())
|
.append("uuid", getUUID())
|
||||||
|
.append("portable", isPortable())
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,11 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return server.getUrl() + ":" + super.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(super.hashCode(), server.hashCode());
|
return Objects.hash(super.hashCode(), server.hashCode());
|
||||||
@ -163,7 +168,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
|
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
|
||||||
return false;
|
return false;
|
||||||
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
||||||
return characterUUID.equals(another.characterUUID) && server.equals(another.server);
|
return isPortable() == another.isPortable()
|
||||||
|
&& characterUUID.equals(another.characterUUID) && server.equals(another.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -77,6 +77,11 @@ public class MicrosoftAccount extends OAuthAccount {
|
|||||||
return session.getProfile().getId();
|
return session.getProfile().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return "microsoft:" + getUUID();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthInfo logIn() throws AuthenticationException {
|
public AuthInfo logIn() throws AuthenticationException {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
@ -163,6 +168,6 @@ public class MicrosoftAccount extends OAuthAccount {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
MicrosoftAccount that = (MicrosoftAccount) o;
|
MicrosoftAccount that = (MicrosoftAccount) o;
|
||||||
return characterUUID.equals(that.characterUUID);
|
return this.isPortable() == that.isPortable() && characterUUID.equals(that.characterUUID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,11 @@ public class OfflineAccount extends Account {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return username + ":" + username;
|
||||||
|
}
|
||||||
|
|
||||||
public Skin getSkin() {
|
public Skin getSkin() {
|
||||||
return skin;
|
return skin;
|
||||||
}
|
}
|
||||||
@ -222,6 +227,6 @@ public class OfflineAccount extends Account {
|
|||||||
if (!(obj instanceof OfflineAccount))
|
if (!(obj instanceof OfflineAccount))
|
||||||
return false;
|
return false;
|
||||||
OfflineAccount another = (OfflineAccount) obj;
|
OfflineAccount another = (OfflineAccount) obj;
|
||||||
return username.equals(another.username);
|
return isPortable() == another.isPortable() && username.equals(another.username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,11 @@ public class YggdrasilAccount extends ClassicAccount {
|
|||||||
return session.getSelectedProfile().getId();
|
return session.getSelectedProfile().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return getUsername() + ":" + getUUID();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
@ -223,6 +228,6 @@ public class YggdrasilAccount extends ClassicAccount {
|
|||||||
if (obj == null || obj.getClass() != YggdrasilAccount.class)
|
if (obj == null || obj.getClass() != YggdrasilAccount.class)
|
||||||
return false;
|
return false;
|
||||||
YggdrasilAccount another = (YggdrasilAccount) obj;
|
YggdrasilAccount another = (YggdrasilAccount) obj;
|
||||||
return characterUUID.equals(another.characterUUID);
|
return isPortable() == another.isPortable() && characterUUID.equals(another.characterUUID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user