e89f3de748
- selectedNodeCodeProvider 改为持久化 StateNotifier(select() 落盘、启动恢复,默认智能 AUTO); 自动连接复用上次选择:智能就智能、具体就具体。 - 自动连接门控加等「选中节点已恢复」(controller.loaded),避免用到未恢复的默认值。 - 自动连接触发时弹 toast「正在自动连接到 X」(全局 ScaffoldMessenger key)。 - 测试:选中节点 select 落盘 + 重启恢复(具体/智能)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
5.6 KiB
Dart
157 lines
5.6 KiB
Dart
// nodes_provider.dart — 节点清单 + 当前选择 + 实测延迟
|
||
//
|
||
// 从 GET /v1/nodes 拉取真实节点(含 host/port);拉取后后台对各节点做 TCP 握手
|
||
// 实测 per-client 延迟并回填。未登录 / 失败返回空列表(不再伪造演示节点)。
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
import '../models/node.dart';
|
||
import '../services/api_config.dart';
|
||
import '../services/latency_probe.dart';
|
||
import 'auth_provider.dart';
|
||
|
||
// ── 节点列表 AsyncNotifier ────────────────────────────────────────
|
||
|
||
class NodesNotifier extends AsyncNotifier<List<Node>> {
|
||
bool _disposed = false;
|
||
DateTime? _lastFetch;
|
||
|
||
/// 刷新限流:距上次成功拉取不足此间隔则跳过远端(下拉/按钮/进页多次触发只打一次)。
|
||
static const _minInterval = Duration(seconds: 10);
|
||
|
||
@override
|
||
Future<List<Node>> build() async {
|
||
ref.onDispose(() => _disposed = true);
|
||
final auth = ref.watch(authProvider);
|
||
if (!auth.isLoggedIn) return const [];
|
||
final list = await _fetchNodes(auth.accessToken!);
|
||
_lastFetch = DateTime.now();
|
||
unawaited(_measure(list));
|
||
return list;
|
||
}
|
||
|
||
/// 刷新节点列表(下拉/按钮/进页调用)。10s 限流;不清空当前列表(后台拉、好了再换),
|
||
/// 避免进页/下拉时列表闪空;失败保留旧数据。
|
||
Future<void> refresh() async {
|
||
final auth = ref.read(authProvider);
|
||
if (!auth.isLoggedIn) {
|
||
state = const AsyncData([]);
|
||
return;
|
||
}
|
||
if (_lastFetch != null && DateTime.now().difference(_lastFetch!) < _minInterval) {
|
||
return; // 限流:10s 内不重复请求远端
|
||
}
|
||
final result = await AsyncValue.guard(() async {
|
||
final list = await _fetchNodes(auth.accessToken!);
|
||
_lastFetch = DateTime.now();
|
||
unawaited(_measure(list));
|
||
return list;
|
||
});
|
||
if (_disposed) return;
|
||
if (result.hasValue) state = result; // 成功才换;失败保留旧列表
|
||
}
|
||
|
||
/// 后台实测各节点延迟,完成后回填(notifier 未销毁才更新)。
|
||
Future<void> _measure(List<Node> list) async {
|
||
if (list.isEmpty) return;
|
||
final pings = await probeAll(
|
||
[for (final n in list) (uuid: n.uuid, host: n.host, port: n.port)],
|
||
);
|
||
if (_disposed) return;
|
||
final updated = [for (final n in list) n.copyWith(ping: pings[n.uuid] ?? n.ping)];
|
||
state = AsyncData(updated);
|
||
}
|
||
|
||
static Future<List<Node>> _fetchNodes(String accessToken) async {
|
||
final uri = Uri.parse('$kApiBaseUrl/v1/nodes');
|
||
final resp = await http.get(uri, headers: {
|
||
'Authorization': 'Bearer $accessToken',
|
||
}).timeout(const Duration(seconds: 10));
|
||
|
||
if (resp.statusCode != 200) return const [];
|
||
|
||
final body = jsonDecode(resp.body) as Map<String, dynamic>;
|
||
final rawList = body['nodes'] as List<dynamic>? ?? [];
|
||
return rawList.map((e) {
|
||
final m = e as Map<String, dynamic>;
|
||
return Node(
|
||
uuid: m['id'] as String? ?? '',
|
||
code: m['region'] as String? ?? '??',
|
||
nameZh: m['name_zh'] as String? ?? '',
|
||
nameEn: m['name_en'] as String? ?? '',
|
||
tier: m['tier'] as String? ?? 'free',
|
||
host: m['host'] as String? ?? '',
|
||
port: (m['port'] as num?)?.toInt() ?? 0,
|
||
status: m['status'] as String? ?? 'up',
|
||
ping: 0, // 由 _measure 实测回填
|
||
);
|
||
}).toList();
|
||
}
|
||
}
|
||
|
||
final nodesProvider =
|
||
AsyncNotifierProvider<NodesNotifier, List<Node>>(NodesNotifier.new);
|
||
|
||
// ── 当前选中的节点 code;'AUTO' 表示智能选择(默认)─────────────────
|
||
//
|
||
// 持久化:用户的选择(智能 / 具体节点)跨重启保存,启动时恢复,供自动连接复用上次选择。
|
||
const _kSelectedNode = 'selected_node_code';
|
||
|
||
class SelectedNodeController extends StateNotifier<String> {
|
||
SelectedNodeController() : super(kSmartNodeCode) {
|
||
_load();
|
||
}
|
||
|
||
/// 是否已从持久化恢复完(自动连接要等它 true 再读,避免用到未恢复的默认值)。
|
||
bool loaded = false;
|
||
|
||
Future<void> _load() async {
|
||
try {
|
||
final p = await SharedPreferences.getInstance();
|
||
if (!mounted) return;
|
||
final v = p.getString(_kSelectedNode);
|
||
if (v != null && v.isNotEmpty) state = v;
|
||
} catch (_) {
|
||
// 测试/无平台:保留默认(智能)。
|
||
}
|
||
loaded = true;
|
||
}
|
||
|
||
/// 选择节点(智能传 kSmartNodeCode)并持久化。
|
||
Future<void> select(String code) async {
|
||
state = code;
|
||
try {
|
||
final p = await SharedPreferences.getInstance();
|
||
await p.setString(_kSelectedNode, code);
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
final selectedNodeCodeProvider =
|
||
StateNotifierProvider<SelectedNodeController, String>((ref) => SelectedNodeController());
|
||
|
||
/// 是否处于智能选择。
|
||
final isSmartSelectProvider = Provider<bool>(
|
||
(ref) => ref.watch(selectedNodeCodeProvider) == kSmartNodeCode,
|
||
);
|
||
|
||
/// 实际生效的节点:列表为空时返回占位;智能选择取实测延迟最小者(未测得排后)。
|
||
final effectiveNodeProvider = Provider<Node>((ref) {
|
||
final nodes = ref.watch(nodesProvider).valueOrNull ?? const <Node>[];
|
||
if (nodes.isEmpty) return kPlaceholderNode;
|
||
|
||
final code = ref.watch(selectedNodeCodeProvider);
|
||
if (code == kSmartNodeCode) {
|
||
return nodes.reduce((a, b) {
|
||
final pa = a.ping > 0 ? a.ping : 1 << 30;
|
||
final pb = b.ping > 0 ? b.ping : 1 << 30;
|
||
return pa <= pb ? a : b;
|
||
});
|
||
}
|
||
return nodes.firstWhere((n) => n.code == code, orElse: () => nodes.first);
|
||
});
|