// punch_status_banner.dart // // Top-of-connection-page banner that shows whether the current session is a // P2P direct connection (NAT punch succeeded) or a relay fallback (NAT punch // failed, going through the user's 21117 server). User-facing copy makes the // implications clear: // // - P2P: 绿色横幅 + ⚡ icon: "P2P 直连成功 — 走的是两台机器之间的网络,可放心传文件" // - Relay: 橙色横幅 + ⚠ icon: "中继模式 — 视频/控制走服务器 21117 中转,请勿传大文件" // // The status comes from ConnectionType.direct, which the Rust core sets // when the connection is established (see src/client/io_loop.rs:184 — // set_connection_type(secure, direct, stream_type)). // // The banner is reactive: it subscribes to the Rx so it updates // automatically if the connection type changes (e.g. after a re-connect). // // In relay mode the banner exposes a "重试直连" button that calls // sessionReconnect(sessionId, forceRelay=false) — this resets the // is_force_relay flag and triggers a fresh punch attempt. Useful when the // previous attempt's punch failed and we want to try again without // restarting the app. import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../common/shared_state.dart'; import '../../models/model.dart'; class PunchStatusBanner extends StatelessWidget { final String peerId; final SessionID? sessionId; const PunchStatusBanner( {super.key, required this.peerId, this.sessionId}); @override Widget build(BuildContext context) { return Obx(() { final ct = ConnectionTypeState.find(peerId); if (!ct.isValid()) { // Not connected yet — show nothing return const SizedBox.shrink(); } final isDirect = ct.direct.value == ConnectionType.strDirect; if (isDirect) { return _Banner( color: const Color(0xFFE7F5EA), // light green iconColor: const Color(0xFF1E8E3E), icon: Icons.bolt, text: 'P2P 直连成功 — 走的是两台机器之间的网络,可放心传文件', sessionId: sessionId, ); } else { return _Banner( color: const Color(0xFFFFF3E0), // light orange iconColor: const Color(0xFFE65100), icon: Icons.warning_amber_rounded, text: '中继模式 — 视频/控制走服务器 21117 中转,请勿传大文件', sessionId: sessionId, ); } }); } } class _Banner extends StatelessWidget { final Color color; final Color iconColor; final IconData icon; final String text; final SessionID? sessionId; const _Banner({ required this.color, required this.iconColor, required this.icon, required this.text, this.sessionId, }); @override Widget build(BuildContext context) { return Material( color: color, elevation: 2, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: iconColor, size: 18), const SizedBox(width: 8), Flexible( child: Text( text, style: TextStyle( color: iconColor, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 8), TextButton.icon( onPressed: sessionId == null ? null : () => _retryDirect(sessionId!), icon: const Icon(Icons.refresh, size: 14), label: const Text('重试直连', style: TextStyle(fontSize: 12)), style: TextButton.styleFrom( foregroundColor: iconColor, padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 0), minimumSize: const Size(0, 28), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), ), ), ); } void _retryDirect(SessionID sessionId) { // Resets is_force_relay and triggers a fresh punch attempt. bind.sessionReconnect(sessionId: sessionId, forceRelay: false); } }