diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a19986e2c..07340e16b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -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(); final navigationBarKey = GlobalKey(); @@ -1681,13 +1681,12 @@ class LastWindowPosition { this.offsetHeight, this.isMaximized, this.isFullscreen); bool equals(LastWindowPosition other) { - return ( - (width == other.width) && - (height == other.height) && - (offsetWidth == other.offsetWidth) && - (offsetHeight == other.offsetHeight) && - (isMaximized == other.isMaximized) && - (isFullscreen == other.isFullscreen)); + return ((width == other.width) && + (height == other.height) && + (offsetWidth == other.offsetWidth) && + (offsetHeight == other.offsetHeight) && + (isMaximized == other.isMaximized) && + (isFullscreen == other.isFullscreen)); } Map toJson() { @@ -1815,7 +1814,8 @@ Future 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 _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); } } } diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 4fac95c6c..7534fb2a1 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -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) { - msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?', - '', 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?', + '', ffi.dialogManager); + } } abstract class ValidationRule { @@ -1509,56 +1518,71 @@ showSetOSAccount( }); } +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) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + onEscape(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + 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: ffi.auditNote); + 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); - ffi.auditNote = text; close(); } - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - }, - ); - 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 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 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 _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 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((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) { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 53a0483f3..cf91e14d2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -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 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 ================================ diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index e436753c5..6e8f42d4e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -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,21 +1763,23 @@ class _DisplayState extends State<_Display> { groupValue: groupValue, label: 'Scrollbar', onChanged: isOptFixed ? null : onChanged), - _Radio(context, - value: kRemoteScrollStyleEdge, - groupValue: groupValue, - label: 'ScrollEdge', - onChanged: isOptFixed ? null : onChanged), - Offstage( - offstage: groupValue != kRemoteScrollStyleEdge, - child: EdgeThicknessControl( - value: double.tryParse(bind.mainGetUserDefaultOption( - key: kOptionEdgeScrollEdgeThickness)) ?? - 100.0, - onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness) - ? null - : onEdgeScrollEdgeThicknessChanged, - )), + if (!isWeb) ...[ + _Radio(context, + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + label: 'ScrollEdge', + onChanged: isOptFixed ? null : onChanged), + Offstage( + offstage: groupValue != kRemoteScrollStyleEdge, + child: EdgeThicknessControl( + value: double.tryParse(bind.mainGetUserDefaultOption( + key: kOptionEdgeScrollEdgeThickness)) ?? + 100.0, + onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness) + ? null + : onEdgeScrollEdgeThicknessChanged, + )), + ], ]); } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3f555dcaa..6dc89d09f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -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?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi; @override - State createState() => _FileManagerPageState(); + State createState() { + final state = _FileManagerPageState(); + _lastState.value = state; + return state; + } } class _FileManagerPageState extends State @@ -139,12 +147,26 @@ class _FileManagerPageState extends State } } + 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 Flexible(flex: 2, child: statusList()) ], ), - ); + )); }) ]); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 525149889..ed3e9682d 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -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 { 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 { 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 { Future 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; diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 6671d041b..13dca0eaf 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -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?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi; @override - State createState() => _PortForwardPageState(); + State createState() { + final state = _PortForwardPageState(); + _lastState.value = state; + return state; + } } class _PortForwardPageState extends State diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8e14b4f1b..431a36b04 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -73,7 +73,10 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with AutomaticKeepAliveClientMixin, MultiWindowListener, TickerProviderStateMixin { + with + AutomaticKeepAliveClientMixin, + MultiWindowListener, + TickerProviderStateMixin { Timer? _timer; String keyboardMode = "legacy"; bool _isWindowBlur = false; @@ -398,7 +401,7 @@ class _RemotePageState extends State super.build(context); return WillPopScope( onWillPop: () async { - clientClose(sessionId, _ffi.dialogManager); + clientClose(sessionId, _ffi); return false; }, child: MultiProvider(providers: [ diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index ba698bd56..6a9f1e89d 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State { 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 { 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 { Future 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 { 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, diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index f28545415..44cccf112 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -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?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi; @override - State createState() => _TerminalPageState(); + State createState() { + final state = _TerminalPageState(); + _lastState.value = state; + return state; + } } class _TerminalPageState extends State @@ -59,12 +66,13 @@ class _TerminalPageState extends State // Initialize terminal connection WidgetsBinding.instance.addPostFrameCallback((_) { widget.tabController.onSelected?.call(widget.id); - + // 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) && - TerminalConnectionManager.getTerminalCount(widget.id) > 1; - + final isExistingConnection = + TerminalConnectionManager.hasConnection(widget.id) && + TerminalConnectionManager.getTerminalCount(widget.id) > 1; + if (!isExistingConnection) { // First terminal - show loading dialog, wait for onReady _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 00b0758d0..e06dee321 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -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 { }) { 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 { Future 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; diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index 87e6e4327..4be6fdc57 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -360,7 +360,7 @@ class _ViewCameraPageState extends State super.build(context); return WillPopScope( onWillPop: () async { - clientClose(sessionId, _ffi.dialogManager); + clientClose(sessionId, _ffi); return false; }, child: MultiProvider(providers: [ diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart index a31ba0fff..4c04cb8b8 100644 --- a/flutter/lib/desktop/pages/view_camera_tab_page.dart +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -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 { 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 { 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 { Future 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 { 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, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index e48c8548a..bc3757f1e 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1122,23 +1122,25 @@ class _DisplayMenuState extends State<_DisplayMenu> { closeOnActivate: groupValue != kRemoteScrollStyleEdge, ffi: widget.ffi, ), - RdoMenuButton( - child: Text(translate('ScrollEdge')), - value: kRemoteScrollStyleEdge, - groupValue: groupValue, - closeOnActivate: false, - onChanged: widget.ffi.canvasModel.imageOverflow.value - ? (value) => onChangeScrollStyle(value) - : null, - ffi: widget.ffi, - ), - Offstage( - offstage: groupValue != kRemoteScrollStyleEdge, - child: EdgeThicknessControl( - value: edgeScrollEdgeThickness.toDouble(), - onChanged: onChangeEdgeScrollEdgeThickness, - colorScheme: colorScheme, - )), + if (!isWeb) ...[ + RdoMenuButton( + child: Text(translate('ScrollEdge')), + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + closeOnActivate: false, + onChanged: widget.ffi.canvasModel.imageOverflow.value + ? (value) => onChangeScrollStyle(value) + : null, + ffi: widget.ffi, + ), + Offstage( + offstage: groupValue != kRemoteScrollStyleEdge, + child: EdgeThicknessControl( + value: edgeScrollEdgeThickness.toDouble(), + 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, ); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index c63a9c606..c7b183d35 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -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 { leading: Row(children: [ IconButton( icon: Icon(Icons.close), - onPressed: () => - clientClose(gFFI.sessionId, gFFI.dialogManager)), + onPressed: () => clientClose(gFFI.sessionId, gFFI)), ]), centerTitle: true, title: ToggleSwitch( @@ -591,67 +594,67 @@ class _FileManagerViewState extends State { Widget headTools() => Container( child: Row( + children: [ + Expanded(child: Obx(() { + final home = controller.options.value.home; + final isWindows = controller.options.value.isWindows; + return BreadCrumb( + items: getPathBreadCrumbItems(controller.shortPath, isWindows, + () => controller.goToHomeDirectory(), (list) { + var path = ""; + if (home.startsWith(list[0])) { + // absolute path + for (var item in list) { + path = PathUtil.join(path, item, isWindows); + } + } else { + path += home; + for (var item in list) { + path = PathUtil.join(path, item, isWindows); + } + } + controller.openDirectory(path); + }), + divider: Icon(Icons.chevron_right), + overflow: ScrollableOverflow(controller: _breadCrumbScroller), + ); + })), + Row( children: [ - Expanded(child: Obx(() { - final home = controller.options.value.home; - final isWindows = controller.options.value.isWindows; - return BreadCrumb( - items: getPathBreadCrumbItems(controller.shortPath, isWindows, - () => controller.goToHomeDirectory(), (list) { - var path = ""; - if (home.startsWith(list[0])) { - // absolute path - for (var item in list) { - path = PathUtil.join(path, item, isWindows); - } + IconButton( + icon: Icon(Icons.arrow_back), + onPressed: controller.goBack, + ), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: controller.goToParentDirectory, + ), + PopupMenuButton( + tooltip: "", + icon: Icon(Icons.sort), + itemBuilder: (context) { + return SortBy.values + .map((e) => PopupMenuItem( + child: Text(translate(e.toString())), + value: e, + )) + .toList(); + }, + onSelected: (sortBy) { + // If selecting the same sort option, flip the order + // If selecting a different sort option, use ascending order + if (controller.sortBy.value == sortBy) { + ascending.value = !controller.sortAscending; } else { - path += home; - for (var item in list) { - path = PathUtil.join(path, item, isWindows); - } + ascending.value = true; } - controller.openDirectory(path); + controller.changeSortStyle(sortBy, + ascending: ascending.value); }), - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow(controller: _breadCrumbScroller), - ); - })), - Row( - children: [ - IconButton( - icon: Icon(Icons.arrow_back), - onPressed: controller.goBack, - ), - IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: controller.goToParentDirectory, - ), - PopupMenuButton( - tooltip: "", - icon: Icon(Icons.sort), - itemBuilder: (context) { - return SortBy.values - .map((e) => PopupMenuItem( - child: Text(translate(e.toString())), - value: e, - )) - .toList(); - }, - onSelected: (sortBy) { - // If selecting the same sort option, flip the order - // If selecting a different sort option, use ascending order - if (controller.sortBy.value == sortBy) { - ascending.value = !controller.sortAscending; - } else { - ascending.value = true; - } - controller.changeSortStyle(sortBy, ascending: ascending.value); - } - ), - ], - ) ], - )); + ) + ], + )); Widget listTail() => Obx(() => Container( height: 100, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 4aef2c5cb..3a8eacb0a 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -366,7 +366,7 @@ class _RemotePageState extends State 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 with WidgetsBindingObserver { color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(sessionId, gFFI.dialogManager); + clientClose(sessionId, gFFI); }, ), IconButton( diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 831e3ac28..395b77962 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -98,6 +98,7 @@ class _SettingsState extends State 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 with WidgetsBindingObserver { _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices); _enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch); _enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); + _allowAskForNoteAtEndOfConnection = + mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection); } @override @@ -782,6 +785,19 @@ class _SettingsState extends State 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) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index e1e06c26c..17d9bbedb 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -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 ? (GoogleFonts.robotoMono().fontFamily ?? 'monospace') : 'monospace'; + SessionID get sessionId => _ffi.sessionId; + @override void initState() { super.initState(); @@ -82,6 +85,16 @@ class _TerminalPageState extends State @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( diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 87fa8aa66..018d22980 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -197,7 +197,7 @@ class _ViewCameraPageState extends State return WillPopScope( onWillPop: () async { - clientClose(sessionId, gFFI.dialogManager); + clientClose(sessionId, gFFI); return false; }, child: Scaffold( @@ -310,7 +310,7 @@ class _ViewCameraPageState extends State color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(sessionId, gFFI.dialogManager); + clientClose(sessionId, gFFI); }, ), IconButton( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8153c16d2..9c6993632 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -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}) { - msgBox(sessionId, type, title, text, link, dialogManager, - hasCancel: hasCancel, - reconnect: hasRetry ? reconnect : null, - reconnectTimeout: hasRetry ? _reconnects : null); + {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 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 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 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 initializeEdgeScrollEdgeThickness() async { - final savedValue = await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId); + final savedValue = + await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId); if (savedValue != null) { _edgeScrollEdgeThickness = savedValue; @@ -2223,12 +2334,12 @@ class CanvasModel with ChangeNotifier { (Vector2, Vector2) getScrollInfo() { final scrollPixel = Vector2( - _horizontal.hasClients ? _horizontal.position.pixels : 0, - _vertical.hasClients ? _vertical.position.pixels : 0); + _horizontal.hasClients ? _horizontal.position.pixels : 0, + _vertical.hasClients ? _vertical.position.pixels : 0); final max = Vector2( - _horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0, - _vertical.hasClients ? _vertical.position.maxScrollExtent : 0); + _horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0, + _vertical.hasClients ? _vertical.position.maxScrollExtent : 0); return (scrollPixel, max); } @@ -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? displays, }) { closed = false; - auditNote = ''; if (isMobile) mobileReset(); assert( (!(isPortForward && isViewCamera)) && diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 388fba5da..a650fb4ae 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1979,5 +1979,41 @@ class RustdeskImpl { ])); } + Future 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 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 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() {} } diff --git a/src/flutter.rs b/src/flutter.rs index 31793ecb2..f45e4c920 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -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, session_id: &SessionID, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ce9954b14..0a6e62a53 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -254,6 +254,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn SyncReturn { + 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 { + 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 { + 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 { + 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(); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 08b147254..60f5ac2f6 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -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(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 4b69f8404..b7d9bb070 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -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(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 7e36c8b2c..714a3e0e3 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -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(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 131fe2e34..794bd5908 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -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(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 583d752b5..0b9475e00 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -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(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d7d1cf68e..ae5b4ef4b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -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(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f00d8d739..a812698eb 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -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(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ce474b041..a6a972368 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -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(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 339bb7d2c..0d74e0b45 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -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(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 118f71965..f94fc49d4 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -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(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0ab53bbe4..d817e67f5 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -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(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 76875f6ae..b8099e1a1 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index a9d1760c6..29b1a1a3a 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -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(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index dc024c097..860faf43c 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -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(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2b4f4567d..f51a76860 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -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(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index f6283683e..f76bed62c 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -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(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index de342146a..50994aad2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -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 l’utilisation exclusive du mode TCP.\nLorsque cette option est activée, RustDesk n’utilise plus le port UDP 21116 et utilise le port TCP 21116 à la place."), ("server-oss-not-support-tip", "Note : Cette fonctionnalité n’est pas disponible sous la version open-source du serveur RustDesk."), + ("input note here", ""), + ("note-at-conn-end-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 46b80776f..957cfa5a8 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -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(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 96ae7d7a7..05732c30f 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -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(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 9944f9045..3487a1fc5 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -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(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b2d7adb67..423d176f9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -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(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a3211a10a..b2ebe48be 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -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(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d4185681e..8f9dfbfa9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -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(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e3e0222b7..97933fc15 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -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(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9058cc9f7..28de20907 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -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(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 6cf9af6e9..62d7345b3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -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(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index f65d28c52..d9dac635b 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -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(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 6d494de41..406d5b3b9 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -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(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 17eab2207..a97ae4ee5 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -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(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 86c5e4528..e449c25d5 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -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(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5cce1dd60..3732184a1 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -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(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 157735b4d..da5595c05 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -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(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5e7a8e277..e9fb9e4ae 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -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(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c45cec5df..3dae7ebf6 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -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(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8800c1d78..13de072cc 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -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(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index ad27c2dea..a456fa63f 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -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(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6c0b0b5e8..d047dd35c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -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(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 9b3f9aa88..93a9565a8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -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(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ae60765f3..6aa203442 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -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(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index fe6aec30f..8c7badab1 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -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(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index b4aae456a..7219d35ee 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -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(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 89079aaba..726135a94 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -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(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 895d680e8..94b5386a3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -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(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index cfc57a046..981df49a6 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -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(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f968f49fa..cc4ccc0e7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -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(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8b031aaa5..1bf7f3ebc 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -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(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 31722d6a3..8daf4d271 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -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(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 25ed68707..d231ec856 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -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(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9c1b7d946..a082a8a78 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -67,6 +67,8 @@ pub struct Session { // Indicate whether the session is reconnected. // Used to auto start file transfer after reconnection. pub reconnect_count: Arc, + pub last_audit_note: Arc>, + pub audit_guid: Arc>, } #[derive(Clone)] @@ -355,7 +357,10 @@ impl Session { } 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 Session { } 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 Session { 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 Session { 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;