Files
pangolin/client/lib/state/nodes_provider.dart
T
wangjia 4dc4127252 feat(client): 三端布局架构 + macOS 桌面端 + app 图标
三端布局(mobile/tablet/desktop):
- core/responsive/form_factor.dart 形态判定 + shell/ 分发器(home_shell→desktop/mobile)
- desktop_shell 对照 ui_kits/desktop/dapp.jsx: 侧栏204·6项 + 套餐卡 + 顶栏(标题/状态/主题切换) + 连接页居中单列
- 新增组件 nav_sidebar / plan_badge_card / content_top_bar / bottom_tab_bar
- 新增一级页 contact_page / settings_page; navigation_provider(NavView)
- 删除旧 widgets/home_shell.dart(逻辑迁入 shell/)

macOS 桌面端:
- 窗口默认 920×600 + 最小 720×560(MainFlutterWindow.swift)
- app 图标替换为穿山甲(AppIcon.appiconset 全套, 由 app-icon.svg 渲染)

其余(本会话):
- Phase2 接线: auth_api/token_store/auth_provider/vpn_bridge_provider + 真实 connection/nodes
- lucide_icons 兼容补丁(packages/lucide_icons_patched) 修复 IconData final 报错
- 测试修复: connect_passthrough(UTF-8) / harness / golden @Skip
- l10n 新增 settingsTitle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:23:20 +08:00

89 lines
3.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// nodes_provider.dart — 节点清单 + 当前选择
//
// 从 GET /v1/nodes 拉取节点列表;认证后自动刷新。
// 未登录 / 加载中时退回演示节点(kDemoNodes)保证 UI 正常显示。
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import '../models/node.dart';
import 'auth_provider.dart';
// ── API base URL(由 --dart-define 注入)──────────────────────────
const _kApiUrl = String.fromEnvironment(
'PANGOLIN_API_URL',
defaultValue: 'http://localhost:8080',
);
// ── 节点列表 AsyncNotifier ────────────────────────────────────────
class NodesNotifier extends AsyncNotifier<List<Node>> {
@override
Future<List<Node>> build() async {
final auth = ref.watch(authProvider);
if (!auth.isLoggedIn) return kDemoNodes;
return _fetchNodes(auth.accessToken!);
}
Future<void> refresh() async {
state = const AsyncLoading();
final auth = ref.read(authProvider);
if (!auth.isLoggedIn) {
state = AsyncData(kDemoNodes);
return;
}
state = await AsyncValue.guard(() => _fetchNodes(auth.accessToken!));
}
static Future<List<Node>> _fetchNodes(String accessToken) async {
final uri = Uri.parse('$_kApiUrl/v1/nodes');
final resp = await http.get(uri, headers: {
'Authorization': 'Bearer $accessToken',
}).timeout(const Duration(seconds: 10));
if (resp.statusCode != 200) return kDemoNodes;
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',
ping: 0, // 延迟由探针数据填充;MVP 默认 0
);
}).toList();
}
}
final nodesProvider =
AsyncNotifierProvider<NodesNotifier, List<Node>>(NodesNotifier.new);
// ── 当前选中的节点 UUID'AUTO' 表示智能选择(默认)─────────────────
final selectedNodeCodeProvider = StateProvider<String>((ref) => kSmartNodeCode);
/// 是否处于智能选择。
final isSmartSelectProvider = Provider<bool>(
(ref) => ref.watch(selectedNodeCodeProvider) == kSmartNodeCode,
);
/// 实际生效的节点:同步拉取 AsyncValue;未就绪时取 kDemoNodes 第一条。
final effectiveNodeProvider = Provider<Node>((ref) {
final nodesAsync = ref.watch(nodesProvider);
final nodes = nodesAsync.valueOrNull ?? kDemoNodes;
if (nodes.isEmpty) return kDemoNodes.first;
final code = ref.watch(selectedNodeCodeProvider);
if (code == kSmartNodeCode) {
// 延迟最小者;MVP 无真实延迟时取第一条
return nodes.reduce((a, b) => a.ping <= b.ping ? a : b);
}
return nodes.firstWhere((n) => n.code == code, orElse: () => nodes.first);
});