ask for note at end of connection (#13499)

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2025-11-13 23:35:40 +08:00
committed by GitHub
parent 13ee3e907d
commit 296c6df462
72 changed files with 932 additions and 200 deletions

View File

@@ -44,7 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
import 'package:flutter_hbb/native/common.dart'
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_hbb/utils/http_service.dart' as http;
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
@@ -1681,8 +1681,7 @@ class LastWindowPosition {
this.offsetHeight, this.isMaximized, this.isFullscreen);
bool equals(LastWindowPosition other) {
return (
(width == other.width) &&
return ((width == other.width) &&
(height == other.height) &&
(offsetWidth == other.offsetWidth) &&
(offsetHeight == other.offsetHeight) &&
@@ -1815,7 +1814,8 @@ Future<void> saveWindowPosition(WindowType type,
final WindowKey key = (type: type, windowId: windowId);
final bool haveNewWindowPosition = (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
final bool haveNewWindowPosition =
(_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
@@ -1841,10 +1841,11 @@ Future<void> _saveWindowPositionActual(WindowKey key) async {
await bind.setLocalFlutterOption(
k: windowFramePrefix + key.type.name, v: pos.toString());
if ((key.type == WindowType.RemoteDesktop || key.type == WindowType.ViewCamera) &&
if ((key.type == WindowType.RemoteDesktop ||
key.type == WindowType.ViewCamera) &&
key.windowId != null) {
await _saveSessionWindowPosition(
key.type, key.windowId!, pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
await _saveSessionWindowPosition(key.type, key.windowId!,
pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
}
}
}

View File

@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import 'address_book.dart';
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
void clientClose(SessionID sessionId, FFI ffi) async {
if (allowAskForNoteAtEndOfConnection(ffi, true)) {
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection();
} else {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
'', dialogManager);
'', ffi.dialogManager);
}
}
abstract class ValidationRule {
@@ -1509,17 +1518,11 @@ showSetOSAccount(
});
}
showAuditDialog(FFI ffi) async {
final controller = TextEditingController(text: ffi.auditNote);
ffi.dialogManager.show((setState, close, context) {
submit() {
var text = controller.text;
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
ffi.auditNote = text;
close();
}
late final focusNode = FocusNode(
Widget buildNoteTextField({
required TextEditingController controller,
required VoidCallback onEscape,
}) {
final focusNode = FocusNode(
onKey: (FocusNode node, RawKeyEvent evt) {
if (evt.logicalKey.keyLabel == 'Enter') {
if (evt is RawKeyDownEvent) {
@@ -1533,7 +1536,7 @@ showAuditDialog(FFI ffi) async {
}
if (evt.logicalKey.keyLabel == 'Esc') {
if (evt is RawKeyDownEvent) {
close();
onEscape();
}
return KeyEventResult.handled;
} else {
@@ -1542,23 +1545,44 @@ showAuditDialog(FFI ffi) async {
},
);
return TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: translate('input note here'),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.all(12),
),
minLines: 5,
maxLines: null,
maxLength: 256,
controller: controller,
focusNode: focusNode,
).workaroundFreezeLinuxMint();
}
showAuditDialog(FFI ffi) async {
final controller = TextEditingController(
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
ffi.dialogManager.show((setState, close, context) {
submit() {
var text = controller.text;
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
close();
}
return CustomAlertDialog(
title: Text(translate('Note')),
content: SizedBox(
width: 250,
height: 120,
child: TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration.collapsed(
hintText: 'input note here',
),
maxLines: null,
maxLength: 256,
child: buildNoteTextField(
controller: controller,
focusNode: focusNode,
).workaroundFreezeLinuxMint()),
onEscape: close,
)),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
@@ -1569,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
});
}
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
if (ffi == null) {
return false;
}
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
bind
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
.isNotEmpty &&
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
(!closedByControlling ||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
}
// return value: close canceled
// true: return
// false: go on
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
{required String id, required DesktopTabController tabController}) async {
try {
final page =
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
final ffi = (page as dynamic).ffi;
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
return res;
} catch (e) {
debugPrint('Failed to show audit dialog: $e');
return false;
}
}
// return value:
// true: return
// false: go on
Future<bool> showConnEndAuditDialogCloseCanceled(
{required FFI ffi, String? type, String? title, String? text}) async {
final res = await _showConnEndAuditDialogCloseCanceled(
ffi: ffi, type: type, title: title, text: text);
if (res == true) {
return true;
}
return false;
}
// return value:
// true: return
// false / null: go on
Future<bool?> _showConnEndAuditDialogCloseCanceled({
required FFI ffi,
String? type,
String? title,
String? text,
}) async {
final closedByControlling = type == null;
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
if (!showDialog) {
return false;
}
ffi.dialogManager.dismissAll();
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
try {
final apiServer = await bind.mainGetApiServer();
if (apiServer.isEmpty) {
debugPrint('API server is empty, cannot update audit note');
return;
}
final url = '$apiServer/api/audit';
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final body = jsonEncode({
'guid': auditGuid,
'note': note,
});
final response = await http.put(
Uri.parse(url),
headers: headers,
body: body,
);
if (response.statusCode == 200) {
debugPrint('Successfully updated audit note for GUID: $auditGuid');
} else {
debugPrint(
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
}
} catch (e) {
debugPrint('Error updating audit note: $e');
}
}
final controller = TextEditingController();
bool askForNote =
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
bool isInProgress = false;
return await ffi.dialogManager.show<bool>((setState, close, context) {
cancel() {
close(true);
}
set() async {
if (isInProgress) return;
setState(() {
isInProgress = true;
});
var text = controller.text;
if (text.isNotEmpty) {
await updateAuditNoteByGuid(
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
.timeout(const Duration(seconds: 6), onTimeout: () {
debugPrint('updateAuditNoteByGuid timeout after 6s');
});
}
// Save the "ask for note" preference
if (!isOptFixed) {
await mainSetLocalBoolOption(
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
}
}
submit() async {
await set();
close(false);
}
final buttons = [
dialogButton('OK', onPressed: isInProgress ? null : submit)
];
if (type == 'relay-hint' || type == 'relay-hint2') {
buttons.add(dialogButton('Retry', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
}));
if (type == 'relay-hint2') {
buttons.add(dialogButton('Connect via relay', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
}));
}
}
if (closedByControlling) {
buttons.add(dialogButton('Cancel',
onPressed: isInProgress ? null : cancel, isOutline: true));
}
Widget content;
if (closedByControlling) {
content = SelectionArea(
child: msgboxContent(
'info', 'Close', 'Are you sure to close the connection?'));
} else {
content =
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
}
return CustomAlertDialog(
title: null,
content: SizedBox(
width: 350,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
content,
const SizedBox(height: 16),
SizedBox(
height: 120,
child: buildNoteTextField(
controller: controller,
onEscape: cancel,
),
),
if (!isOptFixed) ...[
const SizedBox(height: 8),
InkWell(
onTap: () {
setState(() {
askForNote = !askForNote;
});
},
child: Row(
children: [
Checkbox(
value: askForNote,
onChanged: (value) {
setState(() {
askForNote = value ?? false;
});
},
),
Expanded(
child: Text(
translate('note-at-conn-end-tip'),
style: const TextStyle(fontSize: 13),
),
),
],
),
),
],
if (isInProgress)
const LinearProgressIndicator().marginOnly(top: 4),
],
)),
actions: buttons,
onSubmit: submit,
onCancel: cancel,
);
});
}
void showConfirmSwitchSidesDialog(
SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
dialogManager.show((setState, close, context) {

View File

@@ -160,6 +160,7 @@ const String kOptionEnableTrustedDevices = "enable-trusted-devices";
const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
// network options
const String kOptionAllowWebSocket = "allow-websocket";
@@ -324,7 +325,6 @@ const kRemoteViewStyleAdaptive = 'adaptive';
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
const kRemoteViewStyleCustom = 'custom';
/// [kRemoteScrollStyleAuto] Scroll image auto by position.
const kRemoteScrollStyleAuto = 'scrollauto';
@@ -361,12 +361,14 @@ const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
};
// Scale custom related constants
const String kCustomScalePercentKey = 'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
const String kCustomScalePercentKey =
'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
const int kScaleCustomMinPercent = 5;
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
const int kScaleCustomMaxPercent = 1000;
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100%
const double kScaleCustomDetentEpsilon = 0.006; // snap range around pivot (~0.6%)
const double kScaleCustomDetentEpsilon =
0.006; // snap range around pivot (~0.6%)
const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
// ================================ mobile ================================

View File

@@ -561,6 +561,12 @@ class _GeneralState extends State<_General> {
children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
}
children.add(_OptionCheckBox(
context,
'note-at-conn-end-tip',
kOptionAllowAskForNoteAtEndOfConnection,
isServer: false,
));
return _Card(title: 'Other', children: children);
}
@@ -1757,6 +1763,7 @@ class _DisplayState extends State<_Display> {
groupValue: groupValue,
label: 'Scrollbar',
onChanged: isOptFixed ? null : onChanged),
if (!isWeb) ...[
_Radio(context,
value: kRemoteScrollStyleEdge,
groupValue: groupValue,
@@ -1772,6 +1779,7 @@ class _DisplayState extends State<_Display> {
? null
: onEdgeScrollEdgeThicknessChanged,
)),
],
]);
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
@@ -52,7 +53,7 @@ enum MouseFocusScope {
}
class FileManagerPage extends StatefulWidget {
const FileManagerPage(
FileManagerPage(
{Key? key,
required this.id,
required this.password,
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
final bool? forceRelay;
final String? connToken;
final DesktopTabController? tabController;
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
State<StatefulWidget> createState() {
final state = _FileManagerPageState();
_lastState.value = state;
return state;
}
}
class _FileManagerPageState extends State<FileManagerPage>
@@ -139,12 +147,26 @@ class _FileManagerPageState extends State<FileManagerPage>
}
}
Widget willPopScope(Widget child) {
if (isWeb) {
return WillPopScope(
onWillPop: () async {
clientClose(_ffi.sessionId, _ffi);
return false;
},
child: child,
);
} else {
return child;
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Overlay(key: _overlayKeyState.key, initialEntries: [
OverlayEntry(builder: (_) {
return Scaffold(
return willPopScope(Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row(
children: [
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Flexible(flex: 2, child: statusList())
],
),
);
));
})
]);
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(params['id']),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: params['id'],
tabController: tabController,
)) {
return;
}
tabController.closeBy(params['id']);
},
page: FileManagerPage(
key: ValueKey(params['id']),
id: params['id'],
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: FileManagerPage(
key: ValueKey(id),
id: id,
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) {
tabController.clear();
return true;

View File

@@ -25,7 +25,7 @@ class _PortForward {
}
class PortForwardPage extends StatefulWidget {
const PortForwardPage({
PortForwardPage({
Key? key,
required this.id,
required this.password,
@@ -42,9 +42,16 @@ class PortForwardPage extends StatefulWidget {
final bool? forceRelay;
final bool? isSharedPassword;
final String? connToken;
final SimpleWrapper<State<PortForwardPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi;
@override
State<PortForwardPage> createState() => _PortForwardPageState();
State<PortForwardPage> createState() {
final state = _PortForwardPageState();
_lastState.value = state;
return state;
}
}
class _PortForwardPageState extends State<PortForwardPage>

View File

@@ -73,7 +73,10 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin, MultiWindowListener, TickerProviderStateMixin {
with
AutomaticKeepAliveClientMixin,
MultiWindowListener,
TickerProviderStateMixin {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
@@ -398,7 +401,7 @@ class _RemotePageState extends State<RemotePage>
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager);
clientClose(sessionId, _ffi);
return false;
},
child: MultiProvider(providers: [

View File

@@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: peerId!,
tabController: tabController,
)) {
return;
}
tabController.closeBy(peerId!);
},
page: RemotePage(
key: ValueKey(peerId),
id: peerId!,
@@ -316,7 +324,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
translate('Close'),
style: style,
),
proc: () {
proc: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: key,
tabController: tabController,
)) {
return;
}
tabController.closeBy(key);
cancelFunc();
},
@@ -369,6 +383,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) {
tabController.clear();
return true;
@@ -423,7 +445,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: RemotePage(
key: ValueKey(id),
id: id,

View File

@@ -8,7 +8,7 @@ import 'package:xterm/xterm.dart';
import 'terminal_connection_manager.dart';
class TerminalPage extends StatefulWidget {
const TerminalPage({
TerminalPage({
Key? key,
required this.id,
required this.password,
@@ -25,9 +25,16 @@ class TerminalPage extends StatefulWidget {
final bool? isSharedPassword;
final String? connToken;
final int terminalId;
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
@override
State<TerminalPage> createState() => _TerminalPageState();
State<TerminalPage> createState() {
final state = _TerminalPageState();
_lastState.value = state;
return state;
}
}
class _TerminalPageState extends State<TerminalPage>
@@ -62,7 +69,8 @@ class _TerminalPageState extends State<TerminalPage>
// Check if this is a new connection or additional terminal
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
final isExistingConnection =
TerminalConnectionManager.hasConnection(widget.id) &&
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
if (!isExistingConnection) {

View File

@@ -4,6 +4,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
@@ -62,13 +63,20 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
}) {
final tabKey = '${peerId}_$terminalId';
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
final tabLabel = alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
final tabLabel =
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
return TabInfo(
key: tabKey,
label: tabLabel,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabKey,
tabController: tabController,
)) {
return;
}
// Close the terminal session first
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
if (ffi != null) {
@@ -409,6 +417,14 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) {
tabController.clear();
return true;

View File

@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager);
clientClose(sessionId, _ffi);
return false;
},
child: MultiProvider(providers: [

View File

@@ -6,6 +6,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: peerId!,
tabController: tabController,
)) {
return;
}
tabController.closeBy(peerId!);
},
page: ViewCameraPage(
key: ValueKey(peerId),
id: peerId!,
@@ -287,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
translate('Close'),
style: style,
),
proc: () {
proc: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: key,
tabController: tabController,
)) {
return;
}
tabController.closeBy(key);
cancelFunc();
},
@@ -340,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) {
tabController.clear();
return true;
@@ -393,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: ViewCameraPage(
key: ValueKey(id),
id: id,

View File

@@ -1122,6 +1122,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
ffi: widget.ffi,
),
if (!isWeb) ...[
RdoMenuButton<String>(
child: Text(translate('ScrollEdge')),
value: kRemoteScrollStyleEdge,
@@ -1139,6 +1140,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
onChanged: onChangeEdgeScrollEdgeThickness,
colorScheme: colorScheme,
)),
],
Divider(),
]));
});
@@ -2163,7 +2165,12 @@ class _CloseMenu extends StatelessWidget {
return _IconMenuButton(
assetName: 'assets/close.svg',
tooltip: 'Close',
onPressed: () => closeConnection(id: id),
onPressed: () async {
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection(id: id);
},
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor,
);

View File

@@ -12,7 +12,11 @@ import '../../common/widgets/dialog.dart';
class FileManagerPage extends StatefulWidget {
FileManagerPage(
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
{Key? key,
required this.id,
this.password,
this.isSharedPassword,
this.forceRelay})
: super(key: key);
final String id;
final String? password;
@@ -113,8 +117,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
leading: Row(children: [
IconButton(
icon: Icon(Icons.close),
onPressed: () =>
clientClose(gFFI.sessionId, gFFI.dialogManager)),
onPressed: () => clientClose(gFFI.sessionId, gFFI)),
]),
centerTitle: true,
title: ToggleSwitch(
@@ -645,9 +648,9 @@ class _FileManagerViewState extends State<FileManagerView> {
} else {
ascending.value = true;
}
controller.changeSortStyle(sortBy, ascending: ascending.value);
}
),
controller.changeSortStyle(sortBy,
ascending: ascending.value);
}),
],
)
],

View File

@@ -366,7 +366,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, gFFI.dialogManager);
clientClose(sessionId, gFFI);
return false;
},
child: Scaffold(
@@ -484,7 +484,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose(sessionId, gFFI.dialogManager);
clientClose(sessionId, gFFI);
},
),
IconButton(

View File

@@ -98,6 +98,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _disableUdp = false;
var _enableIpv6Punch = false;
var _isUsingPublicServer = false;
var _allowAskForNoteAtEndOfConnection = false;
_SettingsState() {
_enableAbr = option2bool(
@@ -136,6 +137,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
_allowAskForNoteAtEndOfConnection =
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
}
@override
@@ -782,6 +785,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onPressed: (context) {
showThemeSettings(gFFI.dialogManager);
},
),
SettingsTile.switchTile(
title: Text(translate('note-at-conn-end-tip')),
initialValue: _allowAskForNoteAtEndOfConnection,
onToggle: (v) async {
await mainSetLocalBoolOption(
kOptionAllowAskForNoteAtEndOfConnection, v);
final newValue = mainGetLocalBoolOptionSync(
kOptionAllowAskForNoteAtEndOfConnection);
setState(() {
_allowAskForNoteAtEndOfConnection = newValue;
});
},
)
]),
if (isAndroid)

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -38,6 +39,8 @@ class _TerminalPageState extends State<TerminalPage>
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
: 'monospace';
SessionID get sessionId => _ffi.sessionId;
@override
void initState() {
super.initState();
@@ -82,6 +85,16 @@ class _TerminalPageState extends State<TerminalPage>
@override
Widget build(BuildContext context) {
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi);
return false; // Prevent default back behavior
},
child: buildBody(),
);
}
Widget buildBody() {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: TerminalView(

View File

@@ -197,7 +197,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, gFFI.dialogManager);
clientClose(sessionId, gFFI);
return false;
},
child: Scaffold(
@@ -310,7 +310,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose(sessionId, gFFI.dialogManager);
clientClose(sessionId, gFFI);
},
),
IconButton(

View File

@@ -30,6 +30,7 @@ import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
import 'package:tuple/tuple.dart';
import 'package:image/image.dart' as img2;
import 'package:flutter_svg/flutter_svg.dart';
@@ -933,11 +934,21 @@ class FfiModel with ChangeNotifier {
/// Show a message box with [type], [title] and [text].
showMsgBox(SessionID sessionId, String type, String title, String text,
String link, bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) {
{bool? hasCancel}) async {
final showNoteEdit = parent.target != null &&
allowAskForNoteAtEndOfConnection(parent.target, false) &&
(title == "Connection Error" || type == "restarting") &&
!hasRetry;
if (showNoteEdit) {
await showConnEndAuditDialogCloseCanceled(
ffi: parent.target!, type: type, title: title, text: text);
closeConnection();
} else {
msgBox(sessionId, type, title, text, link, dialogManager,
hasCancel: hasCancel,
reconnect: hasRetry ? reconnect : null,
reconnectTimeout: hasRetry ? _reconnects : null);
}
_timer?.cancel();
if (hasRetry) {
_timer = Timer(Duration(seconds: _reconnects), () {
@@ -958,8 +969,30 @@ class FfiModel with ChangeNotifier {
onCancel: closeConnection);
}
void showRelayHintDialog(SessionID sessionId, String type, String title,
String text, OverlayDialogManager dialogManager, String peerId) {
Future<void> showRelayHintDialog(
SessionID sessionId,
String type,
String title,
String text,
OverlayDialogManager dialogManager,
String peerId) async {
var hint = "\n\n${translate('relay_hint_tip')}";
if (text.contains("10054") || text.contains("104")) {
hint = "";
}
final text2 = "${translate(text)}$hint";
if (parent.target != null &&
allowAskForNoteAtEndOfConnection(parent.target, false) &&
pi.isSet.isTrue) {
if (await showConnEndAuditDialogCloseCanceled(
ffi: parent.target!, type: type, title: title, text: text2)) {
return;
}
closeConnection();
return;
}
dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
onClose() {
closeConnection();
@@ -968,13 +1001,10 @@ class FfiModel with ChangeNotifier {
final style =
ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
var hint = "\n\n${translate('relay_hint_tip')}";
if (text.contains("10054") || text.contains("104")) {
hint = "";
}
return CustomAlertDialog(
title: null,
content: msgboxContent(type, title, "${translate(text)}$hint"),
content: msgboxContent(type, title, text2),
actions: [
dialogButton('Close', onPressed: onClose, isOutline: true),
if (type == 'relay-hint')
@@ -1064,10 +1094,91 @@ class FfiModel with ChangeNotifier {
}
}
void _queryAuditGuid(String peerId) async {
try {
if (!mainGetLocalBoolOptionSync(
kOptionAllowAskForNoteAtEndOfConnection)) {
return;
}
if (bind.sessionGetAuditGuid(sessionId: sessionId).isNotEmpty) {
debugPrint('Get cached audit GUID');
return;
}
final url = bind.sessionGetAuditServerSync(
sessionId: sessionId, typ: "conn/active");
if (url.isEmpty) {
return;
}
final initialConnSessionId =
bind.sessionGetConnSessionId(sessionId: sessionId);
final connType = switch (parent.target?.connType) {
ConnType.defaultConn => 0,
ConnType.fileTransfer => 1,
ConnType.portForward => 2,
ConnType.rdp => 2,
ConnType.viewCamera => 3,
ConnType.terminal => 4,
_ => 0,
};
const retryIntervals = [1, 1, 2, 2, 3, 3];
for (int attempt = 1; attempt <= retryIntervals.length; attempt++) {
final currentConnSessionId =
bind.sessionGetConnSessionId(sessionId: sessionId);
if (currentConnSessionId != initialConnSessionId) {
debugPrint('connSessionId changed, stopping audit GUID query');
return;
}
final fullUrl =
'$url?id=$peerId&session_id=$currentConnSessionId&conn_type=$connType';
debugPrint(
'Querying audit GUID, attempt $attempt/${retryIntervals.length}');
try {
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final response = await http.get(
Uri.parse(fullUrl),
headers: headers,
);
if (response.statusCode == 200) {
final guid = jsonDecode(response.body) as String?;
if (guid != null && guid.isNotEmpty) {
bind.sessionSetAuditGuid(sessionId: sessionId, guid: guid);
debugPrint('Successfully retrieved audit GUID');
return;
}
} else {
debugPrint(
'Failed to query audit GUID. Status: ${response.statusCode}, Body: ${response.body}');
return;
}
} catch (e) {
debugPrint('Error querying audit GUID (attempt $attempt): $e');
}
if (attempt < retryIntervals.length) {
await Future.delayed(Duration(seconds: retryIntervals[attempt - 1]));
}
}
debugPrint(
'Failed to retrieve audit GUID after ${retryIntervals.length} attempts');
} catch (e) {
debugPrint('Error in _queryAuditGuid: $e');
}
}
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_queryAuditGuid(peerId);
// This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode();
@@ -2096,9 +2207,8 @@ class CanvasModel with ChangeNotifier {
Future<void> updateScrollStyle() async {
final style = await bind.sessionGetScrollStyle(sessionId: sessionId);
_scrollStyle = style != null
? ScrollStyle.fromString(style)
: ScrollStyle.scrollauto;
_scrollStyle =
style != null ? ScrollStyle.fromString(style) : ScrollStyle.scrollauto;
if (_scrollStyle != ScrollStyle.scrollauto) {
_resetScroll();
@@ -2108,7 +2218,8 @@ class CanvasModel with ChangeNotifier {
}
Future<void> initializeEdgeScrollEdgeThickness() async {
final savedValue = await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId);
final savedValue =
await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId);
if (savedValue != null) {
_edgeScrollEdgeThickness = savedValue;
@@ -3310,7 +3421,6 @@ class FFI {
var version = '';
var connType = ConnType.defaultConn;
var closed = false;
var auditNote = '';
/// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager();
@@ -3401,7 +3511,6 @@ class FFI {
List<int>? displays,
}) {
closed = false;
auditNote = '';
if (isMobile) mobileReset();
assert(
(!(isPortForward && isViewCamera)) &&

View File

@@ -1979,5 +1979,41 @@ class RustdeskImpl {
]));
}
Future<int?> sessionGetEdgeScrollEdgeThickness(
{required UuidValue sessionId, dynamic hint}) {
final thickness = js.context.callMethod(
'getByName', ['option:session', 'edge-scroll-edge-thickness']);
return Future(() => int.tryParse(thickness) ?? 100);
}
Future<void> sessionSetEdgeScrollEdgeThickness(
{required UuidValue sessionId, required int value, dynamic hint}) {
return Future(() => js.context.callMethod('setByName',
['option:session', 'edge-scroll-edge-thickness', value.toString()]));
}
String sessionGetConnSessionId({required UuidValue sessionId, dynamic hint}) {
return js.context.callMethod('getByName', ['conn_session_id']);
}
bool willSessionCloseCloseSession(
{required UuidValue sessionId, dynamic hint}) {
return true;
}
String sessionGetLastAuditNote({required UuidValue sessionId, dynamic hint}) {
return js.context.callMethod('getByName', ['last_audit_note']);
}
Future<void> sessionSetAuditGuid(
{required UuidValue sessionId, required String guid, dynamic hint}) {
return Future(
() => js.context.callMethod('setByName', ['audit_guid', guid]));
}
String sessionGetAuditGuid({required UuidValue sessionId, dynamic hint}) {
return js.context.callMethod('getByName', ['audit_guid']);
}
void dispose() {}
}

View File

@@ -2103,6 +2103,26 @@ pub mod sessions {
s
}
/// Check if removing a session by session_id would result in removing the entire peer.
///
/// Returns:
/// - `true`: The session exists and removing it would leave the peer with no other sessions,
/// so the entire peer would be removed (equivalent to `remove_session_by_session_id` returning `Some`)
/// - `false`: The session doesn't exist, or it exists but the peer has other sessions,
/// so the peer would not be removed (equivalent to `remove_session_by_session_id` returning `None`)
#[inline]
pub fn would_remove_peer_by_session_id(id: &SessionID) -> bool {
for (_peer_key, s) in SESSIONS.read().unwrap().iter() {
let read_lock = s.ui_handler.session_handlers.read().unwrap();
if read_lock.contains_key(id) {
// Found the session, check if it's the only one for this peer
return read_lock.len() == 1;
}
}
// Session not found
false
}
fn check_remove_unused_displays(
current: Option<usize>,
session_id: &SessionID,

View File

@@ -254,6 +254,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn<b
SyncReturn(v)
}
pub fn will_session_close_close_session(session_id: SessionID) -> SyncReturn<bool> {
SyncReturn(sessions::would_remove_peer_by_session_id(&session_id))
}
pub fn session_close(session_id: SessionID) {
if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
// `release_remote_keys` is not required for mobile platforms in common cases.
@@ -1777,6 +1781,36 @@ pub fn session_send_note(session_id: SessionID, note: String) {
}
}
pub fn session_get_last_audit_note(session_id: SessionID) -> SyncReturn<String> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
SyncReturn(session.last_audit_note.lock().unwrap().clone())
} else {
SyncReturn("".to_owned())
}
}
pub fn session_set_audit_guid(session_id: SessionID, guid: String) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
*session.audit_guid.lock().unwrap() = guid;
}
}
pub fn session_get_audit_guid(session_id: SessionID) -> SyncReturn<String> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
SyncReturn(session.audit_guid.lock().unwrap().clone())
} else {
SyncReturn("".to_owned())
}
}
pub fn session_get_conn_session_id(session_id: SessionID) -> SyncReturn<String> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
SyncReturn(session.lc.read().unwrap().session_id.to_string())
} else {
SyncReturn("".to_owned())
}
}
pub fn session_alternative_codecs(session_id: SessionID) -> String {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let (vp8, av1, h264, h265) = session.alternative_codecs();

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", "禁用 UDP"),
("disable-udp-tip", "控制是否仅使用TCP。\n启用此选项后RustDesk 将不再使用UDP 21116而是使用TCP 21116。"),
("server-oss-not-support-tip", "注意RustDesk 开源服务器(OSS server) 不包含此功能。"),
("input note here", "输入备注"),
("note-at-conn-end-tip", "在连接结束时请求备注"),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", "UDP deaktivieren"),
("disable-udp-tip", "Legt fest, ob nur TCP verwendet werden soll. Wenn diese Option aktiviert ist, verwendet RustDesk nicht mehr UDP 21116, sondern stattdessen TCP 21116."),
("server-oss-not-support-tip", "HINWEIS: RustDesk Server OSS enthält diese Funktion nicht."),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -261,5 +261,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("allow-insecure-tls-fallback-tip", "By default, RustDesk verifies the server certificate for protocols using TLS.\nWith this option enabled, RustDesk will fall back to skipping the verification step and proceed in case of verification failure."),
("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."),
("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."),
("note-at-conn-end-tip", "Ask for note at end of connection"),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", "Désactiver UDP"),
("disable-udp-tip", "Contrôle lutilisation exclusive du mode TCP.\nLorsque cette option est activée, RustDesk nutilise plus le port UDP 21116 et utilise le port TCP 21116 à la place."),
("server-oss-not-support-tip", "Note : Cette fonctionnalité nest pas disponible sous la version open-source du serveur RustDesk."),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", "Disabilita UDP"),
("disable-udp-tip", "Controlla se usare solo TCP.\nQuando questa opzione è abilitata, RustDesk non userà più UDP 21116, verrà invece usato TCP 21116."),
("server-oss-not-support-tip", "NOTA: il sistema operativo del server RustDesk non include questa funzionalità."),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", "UDP uitschakelen"),
("disable-udp-tip", "Controleert of alleen TCP moet worden gebruikt. Als deze optie is ingeschakeld, gebruikt RustDesk niet langer UDP 21116, maar TCP 21116."),
("server-oss-not-support-tip", "Opmerking: Deze functie is niet beschikbaar in de open-sourceversie van de RustDesk-server."),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -727,5 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect();
}

View File

@@ -67,6 +67,8 @@ pub struct Session<T: InvokeUiSession> {
// Indicate whether the session is reconnected.
// Used to auto start file transfer after reconnection.
pub reconnect_count: Arc<AtomicUsize>,
pub last_audit_note: Arc<Mutex<String>>,
pub audit_guid: Arc<Mutex<String>>,
}
#[derive(Clone)]
@@ -355,7 +357,10 @@ impl<T: InvokeUiSession> Session<T> {
}
pub fn save_edge_scroll_edge_thickness(&self, value: i32) {
self.lc.write().unwrap().save_edge_scroll_edge_thickness(value);
self.lc
.write()
.unwrap()
.save_edge_scroll_edge_thickness(value);
}
pub fn save_flutter_option(&self, k: String, v: String) {
@@ -562,9 +567,6 @@ impl<T: InvokeUiSession> Session<T> {
}
pub fn get_audit_server(&self, typ: String) -> String {
if LocalConfig::get_option("access_token").is_empty() {
return "".to_owned();
}
crate::get_audit_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
@@ -576,6 +578,7 @@ impl<T: InvokeUiSession> Session<T> {
let url = self.get_audit_server("conn".to_string());
let id = self.get_id();
let session_id = self.lc.read().unwrap().session_id;
*self.last_audit_note.lock().unwrap() = note.clone();
std::thread::spawn(move || {
send_note(url, id, session_id, note);
});
@@ -1281,6 +1284,8 @@ impl<T: InvokeUiSession> Session<T> {
drop(connection_round_state_lock);
let cloned = self.clone();
*cloned.audit_guid.lock().unwrap() = String::new();
*cloned.last_audit_note.lock().unwrap() = String::new();
// override only if true
if true == force_relay {
self.lc.write().unwrap().force_relay = true;