feat: offer Offline and Microsoft auth; keep authlib injector
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 4m59s
Java CI / build (push) Failing after 3h0m0s

Primary account creation entries are Offline and Microsoft. External
Yggdrasil / authlib-injector servers remain listed and addable for later
use. Offline creation is no longer redirected away or region-restricted.
This commit is contained in:
浩 刘 committed 2026-11-07 20:27:29 +00:00
1 parent 4118030d9c
commit 4cd783d6bb
3 files changed
+37 -57

No files matched your search

@@ -55,8 +55,12 @@ public final class Accounts {
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
/// Account factories offered when creating a new account from the UI.
///
/// Offline login remains loadable for existing accounts, but is no longer offered for creation.
public static final List<AccountFactory<?>> FACTORIES = List.of(FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
/// Primary: Offline and Microsoft. Authlib-injector remains available for external Yggdrasil servers.
public static final List<AccountFactory<?>> FACTORIES = List.of(
FACTORY_OFFLINE,
FACTORY_MICROSOFT,
FACTORY_AUTHLIB_INJECTOR
);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -6,15 +6,15 @@ import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
@@ -31,33 +30,13 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.SettingsManager.userSettings;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
/// Displays account creation methods and the configured account list.
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
/// Whether offline and third-party account creation is restricted.
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
static {
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
if ("false".equals(property)
|| "auto".equals(property) && "CN".equals(RegionUtils.MACHINE_REGION)
|| SettingsManager.userSettings().enableOfflineAccountProperty().get())
RESTRICTED.set(false);
else
userSettings().enableOfflineAccountProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) {
if (newValue) {
userSettings().enableOfflineAccountProperty().removeListener(this);
RESTRICTED.set(false);
}
}
});
}
/// Offline / authlib-injector creation is always allowed for this fork.
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(false);
/// The account entries derived from the configured accounts.
private final @UnmodifiableView ObservableList<AccountListItem> items;
@@ -77,6 +56,12 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
items = MappedObservableList.create(accounts, AccountListItem::new);
selectedAccount = createSelectedItemPropertyFor(items, Account.class);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftIcon(SVG.PERSON);
offlineItem.setOnAction(e -> openCreateAccount(Accounts.FACTORY_OFFLINE));
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setTitle(i18n("account.methods.microsoft"));
@@ -90,14 +75,14 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
});
InvalidationListener buildSidebarListener = observable -> {
boolean restricted = RESTRICTED.get();
getSidebar().clear();
getSidebar()
.startCategory(i18n("account.create").toUpperCase(Locale.ROOT))
.add(offlineItem)
.add(microsoftItem);
// External Yggdrasil / authlib-injector servers stay listed for future use.
for (AuthlibInjectorServer server : authServers) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
@@ -110,24 +95,13 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
}
});
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
var title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.setSubtitle(server.getDisplayHostUrl());
Tooltip tooltip = new Tooltip();
if (restricted) {
tooltip.setText(i18n("account.login.restricted"));
item.setActionDisabled(true);
} else {
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
item.setOnAction(e -> {
if (SettingsManager.isUserGameAccountsReadOnly()) {
confirmOverwriteUserAccounts(() -> Controllers.dialog(new CreateAccountPane(server)));
} else {
Controllers.dialog(new CreateAccountPane(server));
}
});
}
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
item.setOnAction(e -> openCreateAccount(Accounts.getAccountFactoryByAuthlibInjectorServer(server)));
FXUtils.installFastTooltip(item, tooltip);
getSidebar().add(item);
@@ -138,7 +112,6 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
WeakInvalidationListener weakBuildSidebarListener = holder.weak(buildSidebarListener);
authServers.addListener(weakBuildSidebarListener);
RESTRICTED.addListener(weakBuildSidebarListener);
AdvancedListItem addAuthServerItem = new AdvancedListItem();
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
@@ -171,6 +144,14 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
setContent(scrollPane);
}
private void openCreateAccount(AccountFactory<?> factory) {
if (SettingsManager.isUserGameAccountsReadOnly()) {
confirmOverwriteUserAccounts(() -> Controllers.dialog(new CreateAccountPane(factory)));
} else {
Controllers.dialog(new CreateAccountPane(factory));
}
}
/// Returns the account selection shared with the launcher.
public ObjectProperty<@Nullable Account> selectedAccountProperty() {
return selectedAccount;
@@ -81,20 +81,15 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
public CreateAccountPane(AccountFactory<?> factory) {
if (factory == null) {
if (AccountListPage.RESTRICTED.get()) {
showMethodSwitcher = false;
factory = Accounts.FACTORY_MICROSOFT;
} else {
showMethodSwitcher = true;
String preferred = settings().preferredLoginTypeProperty().get();
try {
factory = Accounts.getAccountFactory(preferred);
if (factory == Accounts.FACTORY_OFFLINE) {
factory = Accounts.FACTORY_MICROSOFT;
}
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_MICROSOFT;
showMethodSwitcher = true;
String preferred = settings().preferredLoginTypeProperty().get();
try {
factory = Accounts.getAccountFactory(preferred);
if (!Accounts.FACTORIES.contains(factory)) {
factory = Accounts.FACTORY_OFFLINE;
}
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
}
} else {
showMethodSwitcher = false;