From d032b3c6774e13fce1bbe187db7f7db34a4ce44e Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 30 Dec 2022 02:03:19 +0800 Subject: [PATCH] =?UTF-8?q?close=20#1376:=20=E5=9C=A8=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9=E4=B8=AD=E5=AD=98=E5=82=A8=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF=20(#1941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../org/jackhuang/hmcl/setting/Accounts.java | 230 ++++++++++++------ .../org/jackhuang/hmcl/setting/Config.java | 17 +- .../hmcl/setting/ConfigUpgrader.java | 17 -- .../jackhuang/hmcl/setting/GlobalConfig.java | 31 +-- .../org/jackhuang/hmcl/ui/Controllers.java | 2 +- .../hmcl/ui/account/AccountListItem.java | 5 +- .../hmcl/ui/account/AccountListItemSkin.java | 36 +++ .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 3 + .../assets/lang/I18N_zh_CN.properties | 3 + .../java/org/jackhuang/hmcl/auth/Account.java | 38 ++- .../AuthlibInjectorAccount.java | 8 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 7 +- .../hmcl/auth/offline/OfflineAccount.java | 7 +- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 7 +- 15 files changed, 288 insertions(+), 126 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index 494c561cb..b1a403969 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -17,11 +17,12 @@ */ package org.jackhuang.hmcl.setting; +import com.google.gson.reflect.TypeToken; +import javafx.beans.InvalidationListener; 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.FXCollections; import javafx.collections.ObservableList; import org.jackhuang.hmcl.Metadata; 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.game.OAuthServer; 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 java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.logging.Level; import static java.util.stream.Collectors.toList; @@ -130,59 +134,20 @@ public final class Accounts { throw new IllegalArgumentException("Failed to determine account type: " + account); } + private static final String GLOBAL_PREFIX = "$GLOBAL:"; + private static final ObservableList> globalAccountStorages = FXCollections.observableArrayList(); + private static final ObservableList accounts = observableArrayList(account -> new Observable[] { account }); - private static final ReadOnlyListWrapper accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts); - - private static final ObjectProperty selectedAccount = new SimpleObjectProperty(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(); - } - }; + private static final ObjectProperty selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount"); /** * True if {@link #init()} hasn't been called. */ private static boolean initialized = false; - static { - accounts.addListener(onInvalidating(Accounts::updateAccountStorages)); - } - private static Map getAccountStorage(Account account) { Map storage = account.toStorage(); storage.put("type", getLoginType(getAccountFactory(account))); - if (account == selectedAccount.get()) { - storage.put("selected", true); - } return storage; } @@ -192,7 +157,67 @@ public final class Accounts { if (!initialized) return; // update storage - config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList())); + + ArrayList> global = new ArrayList<>(); + ArrayList> portable = new ArrayList<>(); + + for (Account account : accounts) { + Map 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>) + Config.CONFIG_GSON.fromJson(reader, new TypeToken>>() { + }.getType())); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to load global accounts", e); + } + } + + InvocationDispatcher 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 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) 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); + loadGlobalAccountStorages(); - if (Boolean.TRUE.equals(storage.get("selected"))) { - selectedAccount.set(account); + // load accounts + Account selected = null; + for (Map 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 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; config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts)); - Account selected = selectedAccount.get(); if (selected != null) { + Account finalSelected = selected; Schedulers.io().execute(() -> { try { - selected.logIn(); + finalSelected.logIn(); } 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; } - public static ReadOnlyListProperty accountsProperty() { - return accountsWrapper.getReadOnlyProperty(); - } - public static Account getSelectedAccount() { return selectedAccount.get(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 87789663d..5d33a83e5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -51,7 +51,7 @@ public final class Config implements Cloneable, Observable { 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(ObservableList.class, new ObservableListCreator()) .registerTypeAdapter(ObservableSet.class, new ObservableSetCreator()) @@ -142,6 +142,9 @@ public final class Config implements Cloneable, Observable { @SerializedName("configurations") private SimpleMapProperty configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>())); + @SerializedName("selectedAccount") + private StringProperty selectedAccount = new SimpleStringProperty(); + @SerializedName("accounts") private ObservableList> accountStorages = FXCollections.observableArrayList(); @@ -479,6 +482,18 @@ public final class Config implements Cloneable, Observable { return configurations; } + public String getSelectedAccount() { + return selectedAccount.get(); + } + + public void setSelectedAccount(String selectedAccount) { + this.selectedAccount.set(selectedAccount); + } + + public StringProperty selectedAccountProperty() { + return selectedAccount; + } + public ObservableList> getAccountStorages() { return accountStorages; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigUpgrader.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigUpgrader.java index 2b2bf6ad9..9e519b092 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigUpgrader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigUpgrader.java @@ -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)); - }); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java index 06baba378..f6ec16b78 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java @@ -21,42 +21,23 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.property.*; -import javafx.collections.ObservableList; -import javafx.collections.ObservableMap; -import javafx.collections.ObservableSet; -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 javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.lang.reflect.Type; -import java.net.Proxy; import java.util.*; @JsonAdapter(GlobalConfig.Serializer.class) 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 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) { return null; } @@ -93,7 +74,7 @@ public class GlobalConfig implements Cloneable, Observable { } public String toJson() { - return CONFIG_GSON.toJson(this); + return Config.CONFIG_GSON.toJson(this); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 7ac8ae7f4..97b555bc2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -89,7 +89,7 @@ public final class Controllers { private static Lazy accountListPage = new Lazy<>(() -> { AccountListPage accountListPage = new AccountListPage(); accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty()); - accountListPage.accountsProperty().bindContent(Accounts.accountsProperty()); + accountListPage.accountsProperty().bindContent(Accounts.getAccounts()); accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers()); return accountListPage; }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index b6965a8d1..ee5eed0ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -73,13 +73,14 @@ public class AccountListItem extends RadioButton { setUserData(account); String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)); + String portableSuffix = account.isPortable() ? ", " + i18n("account.portable") : ""; if (account instanceof AuthlibInjectorAccount) { AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer(); subtitle.bind(Bindings.concat( loginTypeName, ", ", i18n("account.injector.server"), ": ", - Bindings.createStringBinding(server::getName, server))); + Bindings.createStringBinding(server::getName, server), portableSuffix)); } else { - subtitle.set(loginTypeName); + subtitle.set(loginTypeName + portableSuffix); } StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index c1709f9b3..ba13c0aab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -28,6 +28,7 @@ import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.game.TexturesLoader; @@ -90,6 +91,41 @@ public class AccountListItemSkin extends SkinBase { HBox right = new HBox(); 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(); SpinnerPane spinnerRefresh = new SpinnerPane(); spinnerRefresh.getStyleClass().setAll("small-spinner-pane"); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 4c93fce0b..738ccc312 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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.missing=No Accounts 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.password=Password +account.portable=Portable Account account.skin=Skin account.skin.file=Skin File account.skin.model=Model diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 022c54d93..f2e19c8de 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -125,8 +125,11 @@ account.methods.yggdrasil.migration.hint=自 2022 年 3 月 10 日起,Mojang account.methods.yggdrasil.purchase=購買 Minecraft account.missing=沒有遊戲帳戶 account.missing.add=按一下此處加入帳戶 +account.move_to_global=轉換為全域帳戶 +account.move_to_portable=轉換為便攜帳戶 account.not_logged_in=未登入 account.password=密碼 +account.portable=便攜帳戶 account.skin=皮膚 account.skin.file=皮膚圖片檔案 account.skin.model=模型 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 9b354c461..470e9963b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -125,8 +125,11 @@ account.methods.yggdrasil.migration.hint=自 2022 年 3 月 10 日起,Mojang account.methods.yggdrasil.purchase=购买 Minecraft account.missing=没有游戏帐户 account.missing.add=点击此处添加帐户 +account.move_to_global=转换为全局账户 +account.move_to_portable=转换为便携账户 account.not_logged_in=未登录 account.password=密码 +account.portable=便携账户 account.skin=皮肤 account.skin.file=皮肤图片文件 account.skin.model=模型 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index d1b7138a9..8ac98a2a9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -23,12 +23,15 @@ import javafx.beans.Observable; import javafx.beans.binding.Bindings; 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.TextureType; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -71,7 +74,23 @@ public abstract class Account implements Observable { 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 public void addListener(InvalidationListener listener) { @@ -95,12 +114,29 @@ public abstract class Account implements Observable { 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 public String toString() { return new ToStringBuilder(this) .append("username", getUsername()) .append("character", getCharacter()) .append("uuid", getUUID()) + .append("portable", isPortable()) .toString(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java index fe8583555..003ff95c9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java @@ -153,6 +153,11 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return server; } + @Override + public String getIdentifier() { + return server.getUrl() + ":" + super.getIdentifier(); + } + @Override public int hashCode() { return Objects.hash(super.hashCode(), server.hashCode()); @@ -163,7 +168,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { if (obj == null || obj.getClass() != AuthlibInjectorAccount.class) return false; 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 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index d79673855..d9eb451a7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -77,6 +77,11 @@ public class MicrosoftAccount extends OAuthAccount { return session.getProfile().getId(); } + @Override + public String getIdentifier() { + return "microsoft:" + getUUID(); + } + @Override public AuthInfo logIn() throws AuthenticationException { if (!authenticated) { @@ -163,6 +168,6 @@ public class MicrosoftAccount extends OAuthAccount { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MicrosoftAccount that = (MicrosoftAccount) o; - return characterUUID.equals(that.characterUUID); + return this.isPortable() == that.isPortable() && characterUUID.equals(that.characterUUID); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index 3bbbabad9..7d29bf878 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -86,6 +86,11 @@ public class OfflineAccount extends Account { return username; } + @Override + public String getIdentifier() { + return username + ":" + username; + } + public Skin getSkin() { return skin; } @@ -222,6 +227,6 @@ public class OfflineAccount extends Account { if (!(obj instanceof OfflineAccount)) return false; OfflineAccount another = (OfflineAccount) obj; - return username.equals(another.username); + return isPortable() == another.isPortable() && username.equals(another.username); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 6d79bcd27..1482f96b9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -98,6 +98,11 @@ public class YggdrasilAccount extends ClassicAccount { return session.getSelectedProfile().getId(); } + @Override + public String getIdentifier() { + return getUsername() + ":" + getUUID(); + } + @Override public synchronized AuthInfo logIn() throws AuthenticationException { if (!authenticated) { @@ -223,6 +228,6 @@ public class YggdrasilAccount extends ClassicAccount { if (obj == null || obj.getClass() != YggdrasilAccount.class) return false; YggdrasilAccount another = (YggdrasilAccount) obj; - return characterUUID.equals(another.characterUUID); + return isPortable() == another.isPortable() && characterUUID.equals(another.characterUUID); } }