Add P2P/relay status banner + gate file-transfer menu on relay

Custom fork feature: surface NAT-punch result to the user so they know
whether the connection is going through their 21117 server (relay) or
directly between the two machines (P2P).

Three changes:

1. flutter/lib/common/shared_state.dart: add ConnectionType.isP2P getter
   (true = direct, false = relay) and a top-level isDirectConnection(peerId)
   helper that returns true when state is unknown (so UI doesn't briefly
   grey out before the connection establishes).

2. flutter/lib/desktop/widgets/punch_status_banner.dart (new): a small
   Positioned banner at the top of the connection page.
   - P2P success: green  banner: 'P2P 直连成功 — 走的是两台机器之间的网络,可放心传文件'
   - Relay fallback: orange ⚠ banner: '中继模式 — 视频/控制走服务器 21117 中转,请勿传大文件'
   The banner is reactive (Obx on ConnectionType.direct Rx<String>).

3. flutter/lib/common/widgets/toolbar.dart: wrap the 'Transfer file' and
   'TCP tunneling' menu items' onPressed with an isDirectConnection() guard.
   On relay mode, the menu still shows but the click is intercepted with a
   toast explaining why the action is blocked.

4. flutter/lib/desktop/pages/remote_page.dart: add the banner via
   Positioned(top: 0, left: 0, right: 0) at the end of bodyWidget's Stack.

Why this matters: user has 4 users on 5 Mbps server (114.55.133.123).
When NAT punch fails and the session goes through relay, file transfers
would saturate the server bandwidth. Showing the relay state + blocking
the file-transfer menu protects the server without requiring the user
to remember the technical detail.
This commit is contained in:
xuwenwei
2026-06-09 01:24:54 +08:00
parent 6d587a840e
commit 8a5ba65132
4 changed files with 141 additions and 2 deletions
@@ -0,0 +1,97 @@
// 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<String> so it updates
// automatically if the connection type changes (e.g. after a re-connect).
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common/shared_state.dart';
class PunchStatusBanner extends StatelessWidget {
final String peerId;
const PunchStatusBanner({super.key, required this.peerId});
@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 直连成功 — 走的是两台机器之间的网络,可放心传文件',
);
} else {
return _Banner(
color: const Color(0xFFFFF3E0), // light orange
iconColor: const Color(0xFFE65100),
icon: Icons.warning_amber_rounded,
text: '中继模式 — 视频/控制走服务器 21117 中转,请勿传大文件',
);
}
});
}
}
class _Banner extends StatelessWidget {
final Color color;
final Color iconColor;
final IconData icon;
final String text;
const _Banner({
required this.color,
required this.iconColor,
required this.icon,
required this.text,
});
@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,
),
),
),
],
),
),
),
);
}
}