feat(client): 记住上次选择的节点 + 自动连接 toast(1.0.24)

- selectedNodeCodeProvider 改为持久化 StateNotifier(select() 落盘、启动恢复,默认智能 AUTO);
  自动连接复用上次选择:智能就智能、具体就具体。
- 自动连接门控加等「选中节点已恢复」(controller.loaded),避免用到未恢复的默认值。
- 自动连接触发时弹 toast「正在自动连接到 X」(全局 ScaffoldMessenger key)。
- 测试:选中节点 select 落盘 + 重启恢复(具体/智能)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 19:31:01 +08:00
parent f2891ff2f9
commit e89f3de748
10 changed files with 113 additions and 7 deletions
+18 -2
View File
@@ -23,6 +23,9 @@ import 'widgets/auth_screen.dart';
import 'widgets/onboarding_screen.dart';
import 'widgets/pangolin_logo.dart';
/// 全局 ScaffoldMessenger key:供无 BuildContext 处(如自动连接)弹 SnackBar/toast。
final rootMessengerKey = GlobalKey<ScaffoldMessengerState>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 桌面端:单实例 + 常驻系统托盘(关闭窗口隐藏到托盘而非退出)。
@@ -98,6 +101,7 @@ class _PangolinAppState extends ConsumerState<PangolinApp> {
final mode = ref.watch(themeModeProvider);
return MaterialApp(
title: '穿山甲',
scaffoldMessengerKey: rootMessengerKey,
debugShowCheckedModeBanner: false,
theme: PangolinTheme.light,
darkTheme: PangolinTheme.dark,
@@ -131,22 +135,33 @@ class _RootFlowState extends ConsumerState<RootFlow> {
logLine('AutoConnect', 'armed; waiting for settings loaded + nodes ready');
ProviderSubscription<AsyncValue<List<Node>>>? nodeSub;
ProviderSubscription<AppSettings>? settingsSub;
ProviderSubscription<String>? selSub;
var decided = false;
void tryGo() {
if (decided) return;
final settings = ref.read(settingsProvider);
final selLoaded = ref.read(selectedNodeCodeProvider.notifier).loaded;
// 直接判 _connect 实际会用的「生效节点」是否就绪(uuid 非空),而非「列表里有任一就绪节点」——
// 否则可能在 effectiveNode 尚是占位/未就绪时就触发,_connect 报「节点尚未就绪」。
// 还要等设置 + 选中节点都从持久化恢复完,否则可能用到未恢复的默认(智能)而非上次选择。
final node = ref.read(effectiveNodeProvider);
if (!settings.loaded || node.uuid.isEmpty) return;
if (!settings.loaded || !selLoaded || node.uuid.isEmpty) return;
decided = true;
nodeSub?.close();
settingsSub?.close();
selSub?.close();
final phase = ref.read(connectionProvider).phase;
logLine('AutoConnect', 'ready: autoConnect=${settings.autoConnect} phase=$phase node=${node.code} uuid=${node.uuid.isEmpty ? "EMPTY" : "ok"}');
if (settings.autoConnect && phase == VpnPhase.off) {
logLine('AutoConnect', 'triggering connect');
logLine('AutoConnect', 'triggering connect${node.code}');
ref.read(connectionProvider.notifier).toggle();
// 自动连接 toast:告知用户「正在自动连接到 X」。
final t = ref.read(appTextProvider);
rootMessengerKey.currentState?.showSnackBar(SnackBar(
content: Text(t.autoConnectingTo(node.localizedName(t.lang))),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
));
} else {
logLine('AutoConnect', 'NOT connecting (autoConnect=${settings.autoConnect} phase=$phase)');
}
@@ -154,6 +169,7 @@ class _RootFlowState extends ConsumerState<RootFlow> {
nodeSub = ref.listenManual(nodesProvider, (_, __) => tryGo());
settingsSub = ref.listenManual(settingsProvider, (_, __) => tryGo());
selSub = ref.listenManual(selectedNodeCodeProvider, (_, __) => tryGo());
tryGo();
}