diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 07e961d..5b1bac9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -492,7 +492,10 @@ class _RemotePageState extends State top: 0, left: 0, right: 0, - child: PunchStatusBanner(peerId: widget.id), + child: PunchStatusBanner( + peerId: widget.id, + sessionId: widget.sessionId, + ), ), ], ); diff --git a/flutter/lib/desktop/widgets/punch_status_banner.dart b/flutter/lib/desktop/widgets/punch_status_banner.dart index fd92f19..04eac98 100644 --- a/flutter/lib/desktop/widgets/punch_status_banner.dart +++ b/flutter/lib/desktop/widgets/punch_status_banner.dart @@ -14,15 +14,24 @@ // // 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; - const PunchStatusBanner({super.key, required this.peerId}); + final SessionID? sessionId; + const PunchStatusBanner( + {super.key, required this.peerId, this.sessionId}); @override Widget build(BuildContext context) { @@ -39,6 +48,7 @@ class PunchStatusBanner extends StatelessWidget { iconColor: const Color(0xFF1E8E3E), icon: Icons.bolt, text: 'P2P 直连成功 — 走的是两台机器之间的网络,可放心传文件', + sessionId: sessionId, ); } else { return _Banner( @@ -46,6 +56,7 @@ class PunchStatusBanner extends StatelessWidget { iconColor: const Color(0xFFE65100), icon: Icons.warning_amber_rounded, text: '中继模式 — 视频/控制走服务器 21117 中转,请勿传大文件', + sessionId: sessionId, ); } }); @@ -57,11 +68,13 @@ class _Banner extends StatelessWidget { 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 @@ -88,10 +101,31 @@ class _Banner extends StatelessWidget { ), ), ), + 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); + } } + +