feat(client): 自动连接开关 + Windows 开机自启改任务计划程序

- 新增「自动连接」设置开关(默认关,持久化):启动 app 进登录态主界面后,等节点就绪
  自动连一次(_RootFlowState 一次性 listenManual nodesProvider → toggle())。全平台生效。
- Windows 开机自启改用 schtasks /rl highest 登录触发任务:本应用 requireAdministrator,
  原注册表 Run 键(非提权)开机会弹 UAC/起不来 → 任务计划静默提权拉起。macOS/Linux 仍用
  launch_at_startup 登录项。
- 设置页功能组加「自动连接」行;新增 autoConnect/autoConnectSub 文案。
- _load 加 mounted 守卫(异步初始化期间已 dispose 不再写 state)。
- 测试:autoConnect 默认关 / _load 读持久化 / setAutoConnect 落盘。

注:移动端真·常连(Android Always-on + iOS/iPad On-Demand,无 UI 自连)是更大的原生活,
已单列 todo #14 后续做;本轮移动端只有「打开 app 自动连接」。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 17:46:06 +08:00
parent 2080062d8c
commit 8584f0459e
7 changed files with 140 additions and 2 deletions
+25
View File
@@ -13,6 +13,9 @@ import 'services/token_store.dart';
import 'shell/home_shell.dart';
import 'state/app_providers.dart';
import 'state/auth_provider.dart';
import 'state/connection_provider.dart';
import 'state/nodes_provider.dart';
import 'state/settings_provider.dart';
import 'system_tray.dart';
import 'widgets/auth_screen.dart';
import 'widgets/onboarding_screen.dart';
@@ -113,6 +116,25 @@ class RootFlow extends ConsumerStatefulWidget {
class _RootFlowState extends ConsumerState<RootFlow> {
final _store = const TokenStore();
bool _forceOnboarding = false;
bool _autoConnectArmed = false; // 自动连接每进程只尝试一次
// 进入登录态主界面后:若开了「自动连接」且当前未连接,等节点就绪后自动连一次。
void _maybeAutoConnect() {
if (_autoConnectArmed) return;
_autoConnectArmed = true;
if (!ref.read(settingsProvider).autoConnect) return;
if (ref.read(connectionProvider).phase != VpnPhase.off) return;
// 节点列表异步加载,等出现可用(uuid 非空)节点再连,避免「节点未就绪」直接失败。
late final ProviderSubscription sub;
sub = ref.listenManual(nodesProvider, (prev, next) {
final ready = (next.valueOrNull ?? const []).any((n) => n.uuid.isNotEmpty);
if (!ready) return;
sub.close();
if (ref.read(connectionProvider).phase == VpnPhase.off) {
ref.read(connectionProvider.notifier).toggle();
}
}, fireImmediately: true);
}
// 登录/注册成功后:未看过引导才进引导,否则直接主界面。
Future<void> _afterAuth() async {
@@ -139,6 +161,9 @@ class _RootFlowState extends ConsumerState<RootFlow> {
if (_forceOnboarding) {
return OnboardingScreen(t: t, onDone: () { _afterOnboarding(); });
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _maybeAutoConnect();
});
return const HomeShell();
}
}