From 68115081789eda388b68c24dd943ba2ed9fa82b0 Mon Sep 17 00:00:00 2001 From: xuwenwei Date: Tue, 9 Jun 2026 11:30:15 +0800 Subject: [PATCH] =?UTF-8?q?Add=20'=E9=87=8D=E8=AF=95=E7=9B=B4=E8=BF=9E'=20?= =?UTF-8?q?button=20to=20punch-status=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In relay mode, the banner now exposes a single button that calls sessionReconnect(sessionId, forceRelay=false). This: - resets the is_force_relay flag in the Session lc state - triggers a fresh punch attempt (with the same direct_failures counter, which only shortens the punch timeout, never skips the attempt) - if punch succeeds, banner flips to green and file transfer re-enables The button is disabled (greyed out) if sessionId is not yet known. User's setup has both Macs with global IPv6 (2409:8a1e::, China Mobile), but punch still falls back to relay. Common cause: CGNAT, double-NAT, or symmetric NAT. The retry button lets the user test network conditions without restarting the app. Removed: my earlier attempt at main_get_logs / session_get_session_id FFI bridges — couldn't find the right log path / session API, and the sessionId is already exposed via RemotePage.sessionId getter. --- flutter/lib/desktop/pages/remote_page.dart | 5 ++- .../desktop/widgets/punch_status_banner.dart | 36 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) 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); + } } + +