From bcd9f8240cb3257fa68a31f838a356b664ce77d1 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Thu, 19 Jul 2018 20:32:35 +0800 Subject: [PATCH 01/12] Extract account factories to static fields --- .../java/org/jackhuang/hmcl/setting/Accounts.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 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 427b8de49..f93fe0e3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -44,16 +44,20 @@ import static org.jackhuang.hmcl.util.Pair.pair; public final class Accounts { private Accounts() {} + public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE; + public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE); + public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory( + new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo, + Accounts::getOrCreateAuthlibInjectorServer); + public static final String OFFLINE_ACCOUNT_KEY = "offline"; public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil"; public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector"; public static final Map> ACCOUNT_FACTORY = mapOf( - pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE), - pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)), - pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory( - new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo, - Accounts::getOrCreateAuthlibInjectorServer)) + pair(OFFLINE_ACCOUNT_KEY, FACTORY_OFFLINE), + pair(YGGDRASIL_ACCOUNT_KEY, FACTORY_YGGDRASIL), + pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, FACTORY_AUTHLIB_INJECTOR) ); public static String getAccountType(Account account) { From 487816ab74e0e1f205dc76a4734194a410725a65 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Thu, 19 Jul 2018 21:15:11 +0800 Subject: [PATCH 02/12] Use AccountFactory in cboType --- .../org/jackhuang/hmcl/ui/AddAccountPane.java | 90 ++++++++++--------- .../resources/assets/lang/I18N.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 1 - .../assets/lang/I18N_zh_CN.properties | 1 - 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index f10c269fb..7a9bc7ff9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -21,7 +21,7 @@ import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.*; import javafx.beans.binding.Bindings; -import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.control.Hyperlink; @@ -51,24 +51,28 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Logging; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.logging.Level; + import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG; import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.logging.Level; - public class AddAccountPane extends StackPane { @FXML private JFXTextField txtUsername; @FXML private JFXPasswordField txtPassword; @FXML private Label lblCreationWarning; @FXML private Label lblPassword; - @FXML private JFXComboBox cboType; + @FXML private JFXComboBox> cboType; @FXML private JFXComboBox cboServers; @FXML private Label lblInjectorServer; @FXML private Hyperlink linkManageInjectorServers; @@ -85,23 +89,35 @@ public class AddAccountPane extends StackPane { cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer)); selectDefaultServer(); - cboType.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector")); + Map, String> type2name = 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"))); + cboType.getItems().setAll(type2name.keySet()); + cboType.setConverter(stringConverter(type2name::get)); cboType.getSelectionModel().select(0); - ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty(); + ReadOnlyObjectProperty> loginType = cboType.getSelectionModel().selectedItemProperty(); - txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0)); + txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE)); lblPassword.visibleProperty().bind(txtPassword.visibleProperty()); - cboServers.visibleProperty().bind(loginTypeIdProperty.isEqualTo(2)); + cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR)); lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty()); linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty()); txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@"))); btnAccept.disableProperty().bind(Bindings.createBooleanBinding( - () -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()), - txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty)); + () -> !( // consider the opposite situation: input is valid + txtUsername.validate() && + // invisible means the field is not needed, neither should it be validated + (!txtPassword.isVisible() || txtPassword.validate()) && + (!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null) + ), + txtUsername.textProperty(), + txtPassword.textProperty(), txtPassword.visibleProperty(), + cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty())); } /** @@ -113,45 +129,33 @@ 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) { + // throw an exception if none is selected + return Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).get(); + } + return null; + } + @FXML private void onCreationAccept() { if (btnAccept.isDisabled()) return; - String username = txtUsername.getText(); - String password = txtPassword.getText(); - Object addtionalData; - - int type = cboType.getSelectionModel().getSelectedIndex(); - AccountFactory factory; - switch (type) { - case 0: - factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); - addtionalData = null; - break; - case 1: - factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); - addtionalData = null; - break; - case 2: - factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); - Optional server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()); - if (server.isPresent()) { - addtionalData = server.get(); - } else { - lblCreationWarning.setText(i18n("account.failed.no_selected_server")); - return; - } - break; - default: - throw new Error(); - } - acceptPane.showSpinner(); lblCreationWarning.setText(""); setDisable(true); - Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData)) + String username = txtUsername.getText(); + String password = txtPassword.getText(); + AccountFactory factory = cboType.getSelectionModel().getSelectedItem(); + Object additionalData = getAuthAdditionalData(); + + Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData)) .finalized(Schedulers.javafx(), variables -> { Settings.INSTANCE.addAccount(variables.get("create_account")); acceptPane.hideSpinner(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ca86a7bfd..9a69307a5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -39,7 +39,6 @@ account.failed.invalid_credentials=Incorrect password, or you are forbidden to l account.failed.invalid_password=Invalid password account.failed.invalid_token=Please log out and re-input your password to login. account.failed.no_character=No character in this account. -account.failed.no_selected_server=No authentication server is selected. account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct. account.injector.add=Add an authentication server account.injector.manage=Manage authentication servers diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 706a9c5aa..7f8c2e010 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄 account.failed.invalid_password=無效的密碼 account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄 account.failed.no_character=該帳號沒有角色 -account.failed.no_selected_server=未選擇認證服務器 account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤 account.injector.add=添加認證服務器 account.injector.manage=管理認證服務器 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 f183968d6..33957fee0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -39,7 +39,6 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录 account.failed.invalid_password=无效的密码 account.failed.invalid_token=请尝试登出并重新输入密码登录 account.failed.no_character=该帐号没有角色 -account.failed.no_selected_server=未选择认证服务器 account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误 account.injector.add=添加认证服务器 account.injector.manage=管理认证服务器 From be8cd13b08d3ca34b57d3d1241bf2cdf24ff7149 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Thu, 19 Jul 2018 21:19:09 +0800 Subject: [PATCH 03/12] Throw IAE when getAccountType() receives an unexpected param --- .../java/org/jackhuang/hmcl/setting/Accounts.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 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 f93fe0e3f..a9793283c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -61,10 +61,14 @@ public final class Accounts { ); public static String getAccountType(Account account) { - if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY; - else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY; - else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY; - else return YGGDRASIL_ACCOUNT_KEY; + if (account instanceof OfflineAccount) + return OFFLINE_ACCOUNT_KEY; + else if (account instanceof AuthlibInjectorAccount) + return AUTHLIB_INJECTOR_ACCOUNT_KEY; + else if (account instanceof YggdrasilAccount) + return YGGDRASIL_ACCOUNT_KEY; + else + throw new IllegalArgumentException("Failed to determine account type: " + account); } static String getAccountId(Account account) { From c38848ea0ec326eb67a0a8f07990ebbfafd45f3b Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Thu, 19 Jul 2018 21:22:48 +0800 Subject: [PATCH 04/12] Rename fields in Accounts --- .../org/jackhuang/hmcl/setting/Accounts.java | 22 +++++++++---------- .../org/jackhuang/hmcl/setting/Settings.java | 2 +- 2 files changed, 12 insertions(+), 12 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 a9793283c..7a7fb6e8a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -50,23 +50,23 @@ public final class Accounts { new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo, Accounts::getOrCreateAuthlibInjectorServer); - public static final String OFFLINE_ACCOUNT_KEY = "offline"; - public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil"; - public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector"; + private static final String TYPE_OFFLINE = "offline"; + private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil"; + private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector"; - public static final Map> ACCOUNT_FACTORY = mapOf( - pair(OFFLINE_ACCOUNT_KEY, FACTORY_OFFLINE), - pair(YGGDRASIL_ACCOUNT_KEY, FACTORY_YGGDRASIL), - pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, FACTORY_AUTHLIB_INJECTOR) + static final Map> TYPE_TO_ACCOUNT_FACTORY = mapOf( + pair(TYPE_OFFLINE, FACTORY_OFFLINE), + pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL), + pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR) ); - public static String getAccountType(Account account) { + static String getAccountType(Account account) { if (account instanceof OfflineAccount) - return OFFLINE_ACCOUNT_KEY; + return TYPE_OFFLINE; else if (account instanceof AuthlibInjectorAccount) - return AUTHLIB_INJECTOR_ACCOUNT_KEY; + return TYPE_AUTHLIB_INJECTOR; else if (account instanceof YggdrasilAccount) - return YGGDRASIL_ACCOUNT_KEY; + return TYPE_YGGDRASIL_ACCOUNT; else throw new IllegalArgumentException("Failed to determine account type: " + account); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index a1b667c2b..4d7fc951f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -70,7 +70,7 @@ public class Settings { for (Iterator> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) { Map settings = iterator.next(); - AccountFactory factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse("")); + AccountFactory factory = Accounts.TYPE_TO_ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse("")); if (factory == null) { LOG.warning("Unrecognized account type, removing: " + settings); iterator.remove(); From 357fef86725ef2e5ac5bc4498018fefeeff12f4d Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 20 Jul 2018 15:59:05 +0800 Subject: [PATCH 05/12] Call initProxy() manually --- .../java/org/jackhuang/hmcl/setting/ProxyManager.java | 8 ++------ .../main/java/org/jackhuang/hmcl/setting/Settings.java | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ProxyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ProxyManager.java index 8f8972ff5..b856bd4ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ProxyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ProxyManager.java @@ -36,7 +36,7 @@ public final class ProxyManager { private ProxyManager() { } - private static final ObjectBinding proxyProperty = Bindings.createObjectBinding( + private static ObjectBinding proxyProperty = Bindings.createObjectBinding( () -> { String host = CONFIG.getProxyHost(); Integer port = Lang.toIntOrNull(CONFIG.getProxyPort()); @@ -59,11 +59,7 @@ public final class ProxyManager { return proxyProperty; } - static { - initProxy(); - } - - private static void initProxy() { + static void init() { proxyProperty.addListener(observable -> updateSystemProxy()); updateSystemProxy(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index 4d7fc951f..7d34525ee 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -66,7 +66,7 @@ public class Settings { firstLaunch = CONFIG.isFirstLaunch(); CONFIG.setFirstLaunch(false); - ProxyManager.getProxy(); // init ProxyManager + ProxyManager.init(); for (Iterator> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) { Map settings = iterator.next(); From 088af981df59a48119d41197ce4325e148b11881 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 20 Jul 2018 16:04:38 +0800 Subject: [PATCH 06/12] Turn properties in SettingsPage from local variables into fields --- .../java/org/jackhuang/hmcl/ui/SettingsPage.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java index 3b88e0a99..f60a38b3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -101,7 +101,10 @@ public final class SettingsPage extends StackPane implements DecoratorPage { @FXML private Pane proxyPane; - { + private ObjectProperty selectedProxyType; + private ObjectProperty backgroundType; + + public SettingsPage() { FXUtils.loadFXML(this, "/assets/fxml/setting.fxml"); FXUtils.smoothScrolling(scroll); @@ -148,7 +151,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage { chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty()); chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty()); - ObjectProperty selectedProxyType = new SimpleObjectProperty(Proxy.Type.HTTP) { + selectedProxyType = new SimpleObjectProperty(Proxy.Type.HTTP) { { invalidated(); } @@ -184,14 +187,14 @@ public final class SettingsPage extends StackPane implements DecoratorPage { FXUtils.installTooltip(btnUpdate, i18n("update.tooltip")); checkUpdate(); - // background + // ==== Background ==== backgroundItem.loadChildren(Collections.singletonList( backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT) )); backgroundItem.setCustomUserData(EnumBackgroundImage.CUSTOM); backgroundItem.getTxtCustom().textProperty().bindBidirectional(CONFIG.backgroundImageProperty()); - ObjectProperty backgroundType = new SimpleObjectProperty(EnumBackgroundImage.DEFAULT) { + backgroundType = new SimpleObjectProperty(EnumBackgroundImage.DEFAULT) { { invalidated(); } @@ -211,8 +214,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage { new When(backgroundType.isEqualTo(EnumBackgroundImage.DEFAULT)) .then(i18n("launcher.background.default")) .otherwise(CONFIG.backgroundImageProperty())); + // ==== - // theme + // ==== Theme ==== JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null); picker.setCustomColorText(i18n("color.custom")); picker.setRecentColorsText(i18n("color.recent")); @@ -224,6 +228,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage { }); themeColorPickerContainer.getChildren().setAll(picker); Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0)); + // ==== } public String getTitle() { From f9e9c9d38bae354a9172d907f907f92a5799ea64 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 20 Jul 2018 16:26:15 +0800 Subject: [PATCH 07/12] Use LinkedHashMap for mapOf --- HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 91ae8ae4b..ab9b97345 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -41,7 +41,7 @@ public final class Lang { */ @SafeVarargs public static Map mapOf(Pair... pairs) { - HashMap map = new HashMap<>(); + Map map = new LinkedHashMap<>(); for (Pair pair : pairs) map.put(pair.getKey(), pair.getValue()); return map; From 170189c34426068114f4e7b5a5d9a10323d92e79 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 20 Jul 2018 20:13:26 +0800 Subject: [PATCH 08/12] Refactor accounts --- .../hmcl/event/AccountAddedEvent.java | 57 ------ .../hmcl/event/AccountLoadingEvent.java | 60 ------ .../jackhuang/hmcl/game/AccountHelper.java | 4 +- .../org/jackhuang/hmcl/setting/Accounts.java | 180 +++++++++++++++- .../org/jackhuang/hmcl/setting/Config.java | 6 +- .../org/jackhuang/hmcl/setting/Settings.java | 156 +------------- .../org/jackhuang/hmcl/ui/AccountPage.java | 6 +- .../org/jackhuang/hmcl/ui/AddAccountPane.java | 31 +-- .../jackhuang/hmcl/ui/LeftPaneController.java | 192 ++++++++++-------- .../java/org/jackhuang/hmcl/ui/MainPage.java | 13 +- .../hmcl/ui/export/ModpackInfoPage.java | 4 +- .../resources/assets/lang/I18N.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 1 - .../assets/lang/I18N_zh_CN.properties | 1 - .../AuthlibInjectorAccount.java | 13 ++ .../hmcl/auth/offline/OfflineAccount.java | 13 ++ .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 12 ++ .../hmcl/util/MappedObservableList.java | 160 +++++++++++++++ 18 files changed, 503 insertions(+), 407 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/event/AccountAddedEvent.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/event/AccountLoadingEvent.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountAddedEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountAddedEvent.java deleted file mode 100644 index 70450d32b..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountAddedEvent.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * 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. - *
- * 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(); - } -} - diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountLoadingEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountLoadingEvent.java deleted file mode 100644 index de4fdc167..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/event/AccountLoadingEvent.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * 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. - *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} - * - * @author huangyuhui - */ -public class AccountLoadingEvent extends Event { - - private final Collection accounts; - - /** - * Constructor. - * - * @param source {@link org.jackhuang.hmcl.setting.Settings} - */ - public AccountLoadingEvent(Object source, Collection accounts) { - super(source); - this.accounts = Collections.unmodifiableCollection(accounts); - } - - - public Collection getAccounts() { - return accounts; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("source", source) - .append("accounts", accounts) - .toString(); - } -} - diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java index d60284fa0..faef60043 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java @@ -24,7 +24,7 @@ import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Scheduler; import org.jackhuang.hmcl.task.Schedulers; @@ -42,7 +42,7 @@ public final class AccountHelper { public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins"); public static void loadSkins() { - for (Account account : Settings.INSTANCE.getAccounts()) { + for (Account account : Accounts.getAccounts()) { if (account instanceof YggdrasilAccount) { new SkinLoadTask((YggdrasilAccount) account, false).start(); } 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 7a7fb6e8a..17e6a6551 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -1,7 +1,7 @@ /* * Hello Minecraft! Launcher. * Copyright (C) 2018 huangyuhui - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -29,14 +29,27 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; + +import javafx.beans.Observable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyListProperty; +import javafx.beans.property.ReadOnlyListWrapper; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; + import java.io.IOException; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; +import static java.util.stream.Collectors.toList; +import static javafx.collections.FXCollections.observableArrayList; import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG; +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; /** * @author huangyuhui @@ -54,13 +67,12 @@ public final class Accounts { private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil"; private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector"; - static final Map> TYPE_TO_ACCOUNT_FACTORY = mapOf( + private static Map> type2factory = mapOf( pair(TYPE_OFFLINE, FACTORY_OFFLINE), pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL), - pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR) - ); + pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR)); - static String getAccountType(Account account) { + private static String accountType(Account account) { if (account instanceof OfflineAccount) return TYPE_OFFLINE; else if (account instanceof AuthlibInjectorAccount) @@ -71,14 +83,134 @@ public final class Accounts { throw new IllegalArgumentException("Failed to determine account type: " + account); } - static String getAccountId(Account account) { - return getAccountId(account.getUsername(), account.getCharacter()); + public static AccountFactory getAccountFactory(Account account) { + return type2factory.get(accountType(account)); } - static String getAccountId(String username, String character) { - return username + ":" + character; + private static String accountId(Account account) { + return account.getUsername() + ":" + account.getCharacter(); } + private static ObservableList accounts = observableArrayList(account -> new Observable[] { account }); + private static ReadOnlyListProperty accountsWrapper = new ReadOnlyListWrapper<>(accounts); + + private static ObjectProperty selectedAccount = new SimpleObjectProperty() { + { + 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 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 getAccounts() { + return accounts; + } + + public static ReadOnlyListProperty accountsProperty() { + return accountsWrapper; + } + + public static Account getSelectedAccount() { + return selectedAccount.get(); + } + + public static void setSelectedAccount(Account selectedAccount) { + Accounts.selectedAccount.set(selectedAccount); + } + + public static ObjectProperty selectedAccountProperty() { + return selectedAccount; + } + + // ==== authlib-injector ==== private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) { return CONFIG.getAuthlibInjectorServers().stream() .filter(server -> url.equals(server.getUrl())) @@ -98,4 +230,34 @@ public final class Accounts { return server; }); } + + /** + * After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed. + * This method performs a check and removes the dangling accounts. + */ + private static void removeDanglingAuthlibInjectorAccounts() { + accounts.stream() + .filter(AuthlibInjectorAccount.class::isInstance) + .map(AuthlibInjectorAccount.class::cast) + .filter(it -> !CONFIG.getAuthlibInjectorServers().contains(it.getServer())) + .collect(toList()) + .forEach(accounts::remove); + } + // ==== + + // ==== Login type name i18n === + private static Map, 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)); + } + // ==== } 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 b1ab82646..e3dbfecb2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -126,7 +126,7 @@ public final class Config implements Cloneable, Observable { private ObservableMap configurations = FXCollections.observableMap(new TreeMap<>()); @SerializedName("accounts") - private ObservableList> accounts = FXCollections.observableArrayList(); + private ObservableList> accountStorages = FXCollections.observableArrayList(); @SerializedName("selectedAccount") private StringProperty selectedAccount = new SimpleStringProperty(""); @@ -361,8 +361,8 @@ public final class Config implements Cloneable, Observable { return configurations; } - public ObservableList> getAccounts() { - return accounts; + public ObservableList> getAccountStorages() { + return accountStorages; } public String getSelectedAccount() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index 7d34525ee..08cfb0532 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -17,16 +17,10 @@ */ package org.jackhuang.hmcl.setting; -import javafx.beans.InvalidationListener; -import javafx.beans.property.ObjectProperty; import javafx.beans.value.ObservableValue; import javafx.scene.text.Font; import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.event.*; import org.jackhuang.hmcl.task.Schedulers; @@ -34,82 +28,37 @@ import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.Locales; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; import java.util.stream.Collectors; -import static java.util.stream.Collectors.toList; import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG; -import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; -import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Logging.LOG; public class Settings { public static final Settings INSTANCE = new Settings(); - private final Map accounts = new ConcurrentHashMap<>(); - private final boolean firstLaunch; - private InvalidationListener accountChangeListener = - source -> CONFIG.getAccounts().setAll( - accounts.values().stream() - .map(account -> { - Map storage = account.toStorage(); - storage.put("type", Accounts.getAccountType(account)); - return storage; - }) - .collect(toList())); - private Settings() { firstLaunch = CONFIG.isFirstLaunch(); CONFIG.setFirstLaunch(false); ProxyManager.init(); - - for (Iterator> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) { - Map settings = iterator.next(); - AccountFactory factory = Accounts.TYPE_TO_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())); + Accounts.init(); checkProfileMap(); - save(); - for (Map.Entry profileEntry : getProfileMap().entrySet()) { profileEntry.getValue().setName(profileEntry.getKey()); profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged); profileEntry.getValue().addPropertyChangedListener(e -> save()); } - Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save))); - CONFIG.addListener(source -> save()); } private void save() { + LOG.info("Saving config"); ConfigHolder.saveConfig(CONFIG); } @@ -145,23 +94,6 @@ public class Settings { CONFIG.setLogLines(logLines); } - /**************************************** - * 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 * ****************************************/ @@ -177,86 +109,6 @@ public class Settings { CONFIG.setDownloadType(index); } - /**************************************** - * ACCOUNTS * - ****************************************/ - - private final ImmediateObjectProperty selectedAccount = new ImmediateObjectProperty(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 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 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 * ****************************************/ @@ -347,8 +199,4 @@ public class Settings { EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles())); onProfileChanged(); } - - public void onAccountLoading() { - EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts())); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java index 16052ebee..e2805b61d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java @@ -35,7 +35,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.AccountHelper; -import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.construct.ComponentList; @@ -97,7 +97,7 @@ public class AccountPage extends StackPane implements DecoratorPage { btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15)); lblCharacter.setText(account.getCharacter()); - lblType.setText(AddAccountPane.accountType(account)); + lblType.setText(Accounts.getAccountTypeName(account)); lblEmail.setText(account.getUsername()); btnRefresh.setVisible(account instanceof YggdrasilAccount); @@ -105,7 +105,7 @@ public class AccountPage extends StackPane implements DecoratorPage { @FXML private void onDelete() { - Settings.INSTANCE.deleteAccount(account); + Accounts.getAccounts().remove(account); Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index 7a9bc7ff9..4d03f2d7f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -33,15 +33,12 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; -import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; @@ -53,7 +50,6 @@ import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Logging; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.logging.Level; @@ -62,8 +58,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG; import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; -import static org.jackhuang.hmcl.util.Lang.mapOf; -import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class AddAccountPane extends StackPane { @@ -89,12 +83,8 @@ public class AddAccountPane extends StackPane { cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer)); selectDefaultServer(); - Map, String> type2name = 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"))); - cboType.getItems().setAll(type2name.keySet()); - cboType.setConverter(stringConverter(type2name::get)); + cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR); + cboType.setConverter(stringConverter(Accounts::getAccountTypeName)); cboType.getSelectionModel().select(0); ReadOnlyObjectProperty> loginType = cboType.getSelectionModel().selectedItemProperty(); @@ -157,7 +147,15 @@ public class AddAccountPane extends StackPane { Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData)) .finalized(Schedulers.javafx(), variables -> { - Settings.INSTANCE.addAccount(variables.get("create_account")); + Account account = variables.get("create_account"); + int oldIndex = Accounts.getAccounts().indexOf(account); + if (oldIndex == -1) { + Accounts.getAccounts().add(account); + } else { + // adding an already-added account + // instead of discarding the new account, we replace the existing account with the new account + Accounts.getAccounts().set(oldIndex, account); + } acceptPane.hideSpinner(); fireEvent(new DialogCloseEvent()); }, exception -> { @@ -276,11 +274,4 @@ public class AddAccountPane extends StackPane { return exception.getClass().getName() + ": " + exception.getLocalizedMessage(); } } - - public static String accountType(Account account) { - if (account instanceof OfflineAccount) return i18n("account.methods.offline"); - else if (account instanceof AuthlibInjectorAccount) return i18n("account.methods.authlib_injector"); - else if (account instanceof YggdrasilAccount) return i18n("account.methods.yggdrasil"); - else throw new Error(i18n("account.methods.no_method") + ": " + account); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index f0d127d4d..06d0beb70 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -21,6 +21,12 @@ import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXPopup; import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.When; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; @@ -49,21 +55,39 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.IconedItem; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.util.Lang; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import org.jackhuang.hmcl.util.MappedObservableList; import java.io.File; -import java.util.HashMap; import java.util.LinkedList; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; +import static javafx.collections.FXCollections.singletonObservableList; +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + public final class LeftPaneController { private final AdvancedListBox leftPane; private final VBox profilePane = new VBox(); private final VBox accountPane = new VBox(); private final IconedItem launcherSettingsItem; - private final VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown")); - private final HashMap items = new HashMap<>(); + + private ListProperty accountItems = new SimpleListProperty<>(); + private ObjectProperty selectedAccount = new SimpleObjectProperty() { + { + accountItems.addListener(onInvalidating(this::invalidated)); + } + + @Override + protected void invalidated() { + Account selected = get(); + accountItems.forEach(item -> item.setSelected( + getAccountFromItem(item) + .map(it -> it == selected) + .orElse(false))); + } + }; public LeftPaneController(AdvancedListBox leftPane) { this.leftPane = leftPane; @@ -90,30 +114,91 @@ public final class LeftPaneController { }))) .add(profilePane); - EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd); - EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading); + // ==== Accounts ==== + // Missing account item + VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown")); + RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem); + missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount()); + missingAccountRippler.setOnMouseClicked(e -> addNewAccount()); + + accountItems.bind( + new When(Accounts.accountsProperty().emptyProperty()) + .then(singletonObservableList(missingAccountRippler)) + .otherwise(MappedObservableList.create(Accounts.getAccounts(), this::createAccountItem))); + Bindings.bindContent(accountPane.getChildren(), accountItems); + + selectedAccount.bindBidirectional(Accounts.selectedAccountProperty()); + // ==== + EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading); EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged); EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions); + } - FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged); - onAccountsLoading(); + // ==== Accounts ==== + private Optional getAccountFromItem(RipplerContainer accountItem) { + return Optional.ofNullable(accountItem.getProperties().get("account")) + .map(Account.class::cast); + } + + private static String accountSubtitle(Account account) { + if (account instanceof OfflineAccount) + return i18n("account.methods.offline"); + else if (account instanceof YggdrasilAccount) + return account.getUsername(); + else + return ""; + } + + private RipplerContainer createAccountItem(Account account) { + VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account)); + RipplerContainer rippler = new RipplerContainer(item); + item.setOnSettingsButtonClicked(e -> { + AccountPage accountPage = new AccountPage(account, item); + JFXPopup popup = new JFXPopup(accountPage); + accountPage.setOnDelete(popup::hide); + popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY()); + }); + rippler.setOnMouseClicked(e -> { + if (e.getButton() == MouseButton.PRIMARY) { + selectedAccount.set(account); + } + }); + rippler.getProperties().put("account", account); + rippler.maxWidthProperty().bind(leftPane.widthProperty()); + + if (account instanceof YggdrasilAccount) { + Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); + item.setImage(image, AccountHelper.getViewport(4)); + } else { + item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4)); + } + + if (account instanceof AuthlibInjectorAccount) { + FXUtils.installTooltip(rippler, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName())); + } + + // update skin + if (account instanceof YggdrasilAccount) { + AccountHelper.refreshSkinAsync((YggdrasilAccount) account) + .subscribe(Schedulers.javafx(), () -> { + Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); + item.setImage(image, AccountHelper.getViewport(4)); + }); + } + + return rippler; + } + + public void checkAccount() { + if (Accounts.getAccounts().isEmpty()) + addNewAccount(); } private void addNewAccount() { Controllers.dialog(new AddAccountPane()); } - - private void onSelectedAccountChanged(Account newAccount) { - Platform.runLater(() -> { - for (Node node : accountPane.getChildren()) { - if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) { - boolean current = Objects.equals(node.getProperties().get("account"), newAccount); - ((RipplerContainer) node).setSelected(current); - } - } - }); - } + // ==== private void onProfileChanged(ProfileChangedEvent event) { Profile profile = event.getProfile(); @@ -143,70 +228,6 @@ public final class LeftPaneController { Platform.runLater(() -> profilePane.getChildren().setAll(list)); } - private static String accountType(Account account) { - if (account instanceof OfflineAccount) return i18n("account.methods.offline"); - else if (account instanceof YggdrasilAccount) return account.getUsername(); - else throw new Error(i18n("account.methods.no_method") + ": " + account); - } - - private void onAccountAdd(AccountAddedEvent event) { - Account account = event.getAccount(); - VersionListItem item = items.get(account); - if (account instanceof YggdrasilAccount) - AccountHelper.refreshSkinAsync((YggdrasilAccount) account) - .subscribe(Schedulers.javafx(), () -> { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); - item.setImage(image, AccountHelper.getViewport(4)); - }); - } - - private void onAccountsLoading() { - LinkedList list = new LinkedList<>(); - items.clear(); - Account selectedAccount = Settings.INSTANCE.getSelectedAccount(); - for (Account account : Settings.INSTANCE.getAccounts()) { - VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account)); - items.put(account, item); - RipplerContainer ripplerContainer = new RipplerContainer(item); - item.setOnSettingsButtonClicked(e -> { - AccountPage accountPage = new AccountPage(account, item); - JFXPopup popup = new JFXPopup(accountPage); - accountPage.setOnDelete(popup::hide); - popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY()); - }); - ripplerContainer.setOnMouseClicked(e -> { - if (e.getButton() == MouseButton.PRIMARY) - Settings.INSTANCE.setSelectedAccount(account); - }); - ripplerContainer.getProperties().put("account", account); - ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty()); - - if (account instanceof YggdrasilAccount) { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); - item.setImage(image, AccountHelper.getViewport(4)); - } else - item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4)); - - if (account instanceof AuthlibInjectorAccount) { - FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName())); - } - - if (selectedAccount == account) - ripplerContainer.setSelected(true); - - list.add(ripplerContainer); - } - - if (Settings.INSTANCE.getAccounts().isEmpty()) { - RipplerContainer container = new RipplerContainer(missingAccountItem); - missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount()); - container.setOnMouseClicked(e -> addNewAccount()); - list.add(container); - } - - Platform.runLater(() -> accountPane.getChildren().setAll(list)); - } - public void showUpdate() { launcherSettingsItem.setText(i18n("update.found")); launcherSettingsItem.setTextFill(Color.RED); @@ -247,9 +268,4 @@ public final class LeftPaneController { } }); } - - public void checkAccount() { - if (Settings.INSTANCE.getAccounts().isEmpty()) - addNewAccount(); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java index f1c30cf78..c4664f3c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.event.RefreshingVersionsEvent; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; import org.jackhuang.hmcl.mod.UnsupportedModpackException; +import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Schedulers; @@ -132,13 +133,13 @@ public final class MainPage extends StackPane implements DecoratorPage { }); item.setVersionName(id); item.setOnLaunchButtonClicked(e -> { - if (Settings.INSTANCE.getSelectedAccount() == null) + if (Accounts.getSelectedAccount() == null) Controllers.getLeftPaneController().checkAccount(); else - LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null); + LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null); }); item.setOnScriptButtonClicked(e -> { - if (Settings.INSTANCE.getSelectedAccount() == null) + if (Accounts.getSelectedAccount() == null) Controllers.dialog(i18n("login.empty_username")); else { FileChooser chooser = new FileChooser(); @@ -150,7 +151,7 @@ public final class MainPage extends StackPane implements DecoratorPage { : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); File file = chooser.showSaveDialog(Controllers.getStage()); if (file != null) - LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file); + LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file); } }); item.setOnSettingsButtonClicked(e -> { @@ -214,10 +215,10 @@ public final class MainPage extends StackPane implements DecoratorPage { }); versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); } else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { - if (Settings.INSTANCE.getSelectedAccount() == null) + if (Accounts.getSelectedAccount() == null) Controllers.dialog(i18n("login.empty_username")); else - LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null); + LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null); } }); File iconFile = repository.getVersionIcon(id); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java index 39613db3c..a72bb0baa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java @@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.wizard.WizardController; @@ -67,7 +67,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage { txtModpackName.textProperty().addListener(e -> checkValidation()); txtModpackAuthor.textProperty().addListener(e -> checkValidation()); txtModpackVersion.textProperty().addListener(e -> checkValidation()); - txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse("")); + txtModpackAuthor.setText(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse("")); lblVersionName.setText(version); List launcherJar = Launcher.getCurrentJarFiles(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9a69307a5..20af97280 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -49,7 +49,6 @@ account.injector.server_url=Server URL account.injector.server_name=Server Name account.methods=Login Type account.methods.authlib_injector=authlib-injector -account.methods.no_method=No login method account.methods.offline=Offline account.methods.yggdrasil=Mojang account.missing=None diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7f8c2e010..994820482 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -49,7 +49,6 @@ account.injector.server_url=服務器地址 account.injector.server_name=服務器名稱 account.methods=登錄方式 account.methods.authlib_injector=authlib-injector 登錄 -account.methods.no_method=沒有登入方式 account.methods.offline=離線模式 account.methods.yggdrasil=正版登錄 account.missing=沒有賬戶 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 33957fee0..7bd085184 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -49,7 +49,6 @@ account.injector.server_url=服务器地址 account.injector.server_name=服务器名称 account.methods=登录方式 account.methods.authlib_injector=authlib-injector 登录 -account.methods.no_method=没有登入方式 account.methods.offline=离线模式 account.methods.yggdrasil=正版登录 account.missing=没有账户 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 6ab45f496..43c40ccb2 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 @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.NetworkUtils; import java.util.Base64; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.logging.Level; @@ -110,4 +111,16 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return server; } + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), server.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AuthlibInjectorAccount)) + return false; + AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj; + return super.equals(another) && server.equals(another.server); + } } 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 f4925d9d5..8b294fd83 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 @@ -105,4 +105,17 @@ public class OfflineAccount extends Account { .append("uuid", uuid) .toString(); } + + @Override + public int hashCode() { + return username.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OfflineAccount)) + return false; + OfflineAccount another = (OfflineAccount) obj; + return username.equals(another.username); + } } 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 4d47c1f8f..b55d5b9c0 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 @@ -169,4 +169,16 @@ public class YggdrasilAccount extends Account { return "YggdrasilAccount[username=" + getUsername() + "]"; } + @Override + public int hashCode() { + return characterUUID.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof YggdrasilAccount)) + return false; + YggdrasilAccount another = (YggdrasilAccount) obj; + return characterUUID.equals(another.characterUUID); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java new file mode 100644 index 000000000..da7684e5b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java @@ -0,0 +1,160 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * 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.HashMap; +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 implements ListChangeListener { + private ObservableList origin; + private ObservableList target; + private Function 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 buffer; + + MappedObservableListUpdater(ObservableList origin, ObservableList target, Function mapper) { + this.origin = origin; + this.target = target; + this.mapper = mapper; + this.buffer = new ArrayList<>(target); + } + + @Override + public void onChanged(Change change) { + // cache removed elements to reduce calls to mapper + Map> cache = new HashMap<>(); + + 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 originRemoved = change.getRemoved(); + List 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> cache, T key, U value) { + cache.computeIfAbsent(key, any -> new LinkedList<>()) + .push(value); + } + + private U map(Map> cache, T key) { + LinkedList 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 ObservableList create(ObservableList origin, Function mapper) { + // create a already-synchronized target ObservableList + ObservableList target = origin.stream() + .map(mapper) + .collect(toCollection(FXCollections::observableArrayList)); + + // then synchronize further changes to target + ListChangeListener 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); + } +} From b820aad3589becc4cb13578accd5b6ed2c6a8c06 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 20 Jul 2018 20:21:39 +0800 Subject: [PATCH 09/12] Use MappedObservableList in AuthlibInjectorServersPage --- .../hmcl/ui/AuthlibInjectorServersPage.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java index 3cdc611bb..96efa7727 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java @@ -17,17 +17,18 @@ */ package org.jackhuang.hmcl.ui; -import static java.util.stream.Collectors.toList; import static org.jackhuang.hmcl.ui.FXUtils.loadFXML; import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.util.MappedObservableList; -import javafx.beans.InvalidationListener; -import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ScrollPane; import javafx.scene.layout.StackPane; @@ -42,26 +43,21 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa @FXML private VBox listPane; @FXML private StackPane contentPane; - private InvalidationListener serversListener; + private ObservableList serverItems; public AuthlibInjectorServersPage() { loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml"); smoothScrolling(scrollPane); - serversListener = observable -> updateServersList(); - CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener)); - updateServersList(); + serverItems = MappedObservableList.create(CONFIG.getAuthlibInjectorServers(), this::createServerItem); + Bindings.bindContent(listPane.getChildren(), serverItems); } - private void updateServersList() { - listPane.getChildren().setAll( - CONFIG.getAuthlibInjectorServers().stream() - .map(server -> new AuthlibInjectorServerItem(server, - item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer()))) - .collect(toList())); + private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) { + return new AuthlibInjectorServerItem(server, + item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())); } - @FXML private void onAdd() { Controllers.dialog(new AddAuthlibInjectorServerPane()); From d30758a2a87fec9e228aef2227caf4eb7939527f Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 21 Jul 2018 09:57:47 +0800 Subject: [PATCH 10/12] Throw NPE when selected server is null --- HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index 4d03f2d7f..1b66d1f4c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -50,10 +50,10 @@ import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Logging; import java.util.List; -import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.logging.Level; +import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG; import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; @@ -125,8 +125,7 @@ public class AddAccountPane extends StackPane { private Object getAuthAdditionalData() { AccountFactory factory = cboType.getSelectionModel().getSelectedItem(); if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) { - // throw an exception if none is selected - return Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).get(); + return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null"); } return null; } From cb6d8a88f1f904edd6c904ac9469006241d54655 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 21 Jul 2018 12:31:08 +0800 Subject: [PATCH 11/12] Fix the old account isn't replaced when adding a same account --- HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java | 5 +++-- .../java/org/jackhuang/hmcl/util/MappedObservableList.java | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index 1b66d1f4c..87d2111ed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -152,8 +152,9 @@ public class AddAccountPane extends StackPane { Accounts.getAccounts().add(account); } else { // adding an already-added account - // instead of discarding the new account, we replace the existing account with the new account - Accounts.getAccounts().set(oldIndex, 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); } acceptPane.hideSpinner(); fireEvent(new DialogCloseEvent()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java index da7684e5b..21ab225eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MappedObservableList.java @@ -22,7 +22,7 @@ import static javafx.collections.FXCollections.unmodifiableObservableList; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -75,7 +75,7 @@ public final class MappedObservableList { @Override public void onChanged(Change change) { // cache removed elements to reduce calls to mapper - Map> cache = new HashMap<>(); + Map> cache = new IdentityHashMap<>(); while (change.next()) { int from = change.getFrom(); From ae64097edf3b8e7bd52937c64eb099524d840233 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 21 Jul 2018 12:34:12 +0800 Subject: [PATCH 12/12] Automatically select the newly added account --- HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index 87d2111ed..a8f1cdcfa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -146,6 +146,7 @@ public class AddAccountPane extends StackPane { Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData)) .finalized(Schedulers.javafx(), variables -> { + Account account = variables.get("create_account"); int oldIndex = Accounts.getAccounts().indexOf(account); if (oldIndex == -1) { @@ -156,6 +157,10 @@ public class AddAccountPane extends StackPane { Accounts.getAccounts().remove(oldIndex); Accounts.getAccounts().add(oldIndex, account); } + + // select the new account + Accounts.setSelectedAccount(account); + acceptPane.hideSpinner(); fireEvent(new DialogCloseEvent()); }, exception -> {