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
@@ -0,0 +1,50 @@
// selected_node_persist_test.dart — 选中节点(智能/具体)跨重启持久化,供自动连接复用上次选择。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pangolin_vpn/models/node.dart';
import 'package:pangolin_vpn/state/nodes_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
Future<void> waitSelected(ProviderContainer c, String want) async {
for (var i = 0; i < 60; i++) {
if (c.read(selectedNodeCodeProvider) == want) return;
await Future<void>.delayed(const Duration(milliseconds: 10));
}
}
test('默认智能选择(AUTO)', () async {
final c = ProviderContainer();
addTearDown(c.dispose);
expect(c.read(selectedNodeCodeProvider), kSmartNodeCode);
});
test('select 具体节点 → 落盘', () async {
final c = ProviderContainer();
addTearDown(c.dispose);
await waitSelected(c, kSmartNodeCode); // 等 _load
await c.read(selectedNodeCodeProvider.notifier).select('JP');
expect(c.read(selectedNodeCodeProvider), 'JP');
final p = await SharedPreferences.getInstance();
expect(p.getString('selected_node_code'), 'JP', reason: '应持久化');
});
test('重启从持久化恢复具体节点', () async {
SharedPreferences.setMockInitialValues({'selected_node_code': 'JP'});
final c = ProviderContainer();
addTearDown(c.dispose);
await waitSelected(c, 'JP');
expect(c.read(selectedNodeCodeProvider), 'JP', reason: '重启应恢复上次选择');
});
test('重启恢复智能选择(AUTO 持久化)', () async {
SharedPreferences.setMockInitialValues({'selected_node_code': kSmartNodeCode});
final c = ProviderContainer();
addTearDown(c.dispose);
await waitSelected(c, kSmartNodeCode);
expect(c.read(selectedNodeCodeProvider), kSmartNodeCode);
});
}