Files
RustDesk/flutter/lib/common/shared_state.dart
T
xuwenwei 8a5ba65132 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.
2026-06-09 01:24:54 +08:00

388 lines
9.9 KiB
Dart

import 'package:flutter_hbb/common.dart';
import 'package:get/get.dart';
import '../consts.dart';
// TODO: A lot of dup code.
class PrivacyModeState {
static String tag(String id) => 'privacy_mode_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs;
Get.put<RxString>(state, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
} else {
Get.find<RxString>(tag: key).value = '';
}
}
static RxString find(String id) => Get.find<RxString>(tag: tag(id));
}
class BlockInputState {
static String tag(String id) => 'block_input_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class CurrentDisplayState {
static String tag(String id) => 'current_display_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0);
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 0;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}
class ConnectionType {
final Rx<String> _secure = kInvalidValueStr.obs;
final Rx<String> _direct = kInvalidValueStr.obs;
final Rx<String> _stream_type = kInvalidValueStr.obs;
Rx<String> get secure => _secure;
Rx<String> get direct => _direct;
Rx<String> get stream_type => _stream_type;
static String get strSecure => 'secure';
static String get strInsecure => 'insecure';
static String get strDirect => '';
static String get strIndirect => '_relay';
void setSecure(bool v) {
_secure.value = v ? strSecure : strInsecure;
}
void setDirect(bool v) {
_direct.value = v ? strDirect : strIndirect;
}
void setStreamType(String v) {
_stream_type.value = v;
}
bool isValid() {
return _secure.value != kInvalidValueStr &&
_direct.value != kInvalidValueStr &&
_stream_type.value != kInvalidValueStr;
}
/// Returns true if the connection is using P2P (direct) transport, i.e. NAT
/// punching succeeded. Returns false if going through the relay server.
/// Returns true (assume P2P) if the connection state isn't known yet, so the
/// UI doesn't briefly grey out before the connection establishes.
bool get isP2P {
if (!isValid()) return true;
return _direct.value == strDirect;
}
}
/// Convenience function: checks the P2P/relay state of a connection by peerId.
/// Returns true (assume P2P) if state is unknown.
bool isDirectConnection(String peerId) {
try {
return ConnectionTypeState.find(peerId).isP2P;
} catch (_) {
return true;
}
}
class ConnectionTypeState {
static String tag(String id) => 'connection_type_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<ConnectionType>(tag: key)) {
final ConnectionType collectionType = ConnectionType();
Get.put<ConnectionType>(collectionType, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<ConnectionType>(tag: key)) {
Get.delete<ConnectionType>(tag: key);
}
}
static ConnectionType find(String id) =>
Get.find<ConnectionType>(tag: tag(id));
}
class FingerprintState {
static String tag(String id) => 'fingerprint_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs;
Get.put<RxString>(state, tag: key);
} else {
Get.find<RxString>(tag: key).value = '';
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
}
}
static RxString find(String id) => Get.find<RxString>(tag: tag(id));
}
class ShowRemoteCursorState {
static String tag(String id) => 'show_remote_cursor_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class ShowRemoteCursorLockState {
static String tag(String id) => 'show_remote_cursor_lock_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class KeyboardEnabledState {
static String tag(String id) => 'keyboard_enabled_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
// Server side, default true
final RxBool state = true.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = true;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class RemoteCursorMovedState {
static String tag(String id) => 'remote_cursor_moved_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class RemoteCountState {
static String tag() => 'remote_count_';
static void init() {
final key = tag();
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = 1.obs;
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 1;
}
}
static void delete() {
final key = tag();
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}
static RxInt find() => Get.find<RxInt>(tag: tag());
}
class PeerBoolOption {
static String tag(String id, String opt) => 'peer_{$opt}_$id';
static void init(String id, String opt, bool Function() init_getter) {
final key = tag(id, opt);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool value = RxBool(init_getter());
Get.put<RxBool>(value, tag: key);
} else {
Get.find<RxBool>(tag: key).value = init_getter();
}
}
static void delete(String id, String opt) {
final key = tag(id, opt);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id, String opt) =>
Get.find<RxBool>(tag: tag(id, opt));
}
class PeerStringOption {
static String tag(String id, String opt) => 'peer_{$opt}_$id';
static void init(String id, String opt, String Function() init_getter) {
final key = tag(id, opt);
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString value = RxString(init_getter());
Get.put<RxString>(value, tag: key);
} else {
Get.find<RxString>(tag: key).value = init_getter();
}
}
static void delete(String id, String opt) {
final key = tag(id, opt);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
}
}
static RxString find(String id, String opt) =>
Get.find<RxString>(tag: tag(id, opt));
}
class UnreadChatCountState {
static String tag(id) => 'unread_chat_count_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0);
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 0;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}
initSharedStates(String id) {
PrivacyModeState.init(id);
BlockInputState.init(id);
CurrentDisplayState.init(id);
KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id);
ShowRemoteCursorLockState.init(id);
RemoteCursorMovedState.init(id);
FingerprintState.init(id);
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
UnreadChatCountState.init(id);
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
}
removeSharedStates(String id) {
PrivacyModeState.delete(id);
BlockInputState.delete(id);
CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id);
ShowRemoteCursorLockState.delete(id);
KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id);
FingerprintState.delete(id);
PeerBoolOption.delete(id, kOptionZoomCursor);
UnreadChatCountState.delete(id);
if (isMobile) ConnectionTypeState.delete(id);
}