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'; if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
import 'package:flutter_hbb/native/common.dart' import 'package:flutter_hbb/native/common.dart'
if (dart.library.html) 'package:flutter_hbb/web/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 globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey(); final navigationBarKey = GlobalKey();
@@ -1681,13 +1681,12 @@ class LastWindowPosition {
this.offsetHeight, this.isMaximized, this.isFullscreen); this.offsetHeight, this.isMaximized, this.isFullscreen);
bool equals(LastWindowPosition other) { bool equals(LastWindowPosition other) {
return ( return ((width == other.width) &&
(width == other.width) && (height == other.height) &&
(height == other.height) && (offsetWidth == other.offsetWidth) &&
(offsetWidth == other.offsetWidth) && (offsetHeight == other.offsetHeight) &&
(offsetHeight == other.offsetHeight) && (isMaximized == other.isMaximized) &&
(isMaximized == other.isMaximized) && (isFullscreen == other.isFullscreen));
(isFullscreen == other.isFullscreen));
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -1815,7 +1814,8 @@ Future<void> saveWindowPosition(WindowType type,
final WindowKey key = (type: type, windowId: windowId); 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; final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) { if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
@@ -1841,10 +1841,11 @@ Future<void> _saveWindowPositionActual(WindowKey key) async {
await bind.setLocalFlutterOption( await bind.setLocalFlutterOption(
k: windowFramePrefix + key.type.name, v: pos.toString()); 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) { key.windowId != null) {
await _saveSessionWindowPosition( await _saveSessionWindowPosition(key.type, key.windowId!,
key.type, key.windowId!, pos.isMaximized ?? false, pos.isFullscreen ?? false, pos); 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/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.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_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
import '../../common.dart'; import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'address_book.dart'; import 'address_book.dart';
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) { void clientClose(SessionID sessionId, FFI ffi) async {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?', if (allowAskForNoteAtEndOfConnection(ffi, true)) {
'', dialogManager); if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection();
} else {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
'', ffi.dialogManager);
}
} }
abstract class ValidationRule { 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 { 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) { ffi.dialogManager.show((setState, close, context) {
submit() { submit() {
var text = controller.text; var text = controller.text;
bind.sessionSendNote(sessionId: ffi.sessionId, note: text); bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
ffi.auditNote = text;
close(); 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( return CustomAlertDialog(
title: Text(translate('Note')), title: Text(translate('Note')),
content: SizedBox( content: SizedBox(
width: 250, width: 250,
height: 120, height: 120,
child: TextField( child: buildNoteTextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration.collapsed(
hintText: 'input note here',
),
maxLines: null,
maxLength: 256,
controller: controller, controller: controller,
focusNode: focusNode, onEscape: close,
).workaroundFreezeLinuxMint()), )),
actions: [ actions: [
dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit) 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( void showConfirmSwitchSidesDialog(
SessionID sessionId, String id, OverlayDialogManager dialogManager) async { SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
dialogManager.show((setState, close, context) { 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 kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale"; const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick"; const String kOptionShowVirtualJoystick = "show-virtual-joystick";
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
// network options // network options
const String kOptionAllowWebSocket = "allow-websocket"; const String kOptionAllowWebSocket = "allow-websocket";
@@ -324,7 +325,6 @@ const kRemoteViewStyleAdaptive = 'adaptive';
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent. /// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
const kRemoteViewStyleCustom = 'custom'; const kRemoteViewStyleCustom = 'custom';
/// [kRemoteScrollStyleAuto] Scroll image auto by position. /// [kRemoteScrollStyleAuto] Scroll image auto by position.
const kRemoteScrollStyleAuto = 'scrollauto'; const kRemoteScrollStyleAuto = 'scrollauto';
@@ -361,12 +361,14 @@ const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
}; };
// Scale custom related constants // 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 kScaleCustomMinPercent = 5;
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
const int kScaleCustomMaxPercent = 1000; const int kScaleCustomMaxPercent = 1000;
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100% 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); const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
// ================================ mobile ================================ // ================================ mobile ================================

View File

@@ -561,6 +561,12 @@ class _GeneralState extends State<_General> {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless)); context, 'Allow linux headless', kOptionAllowLinuxHeadless));
} }
children.add(_OptionCheckBox(
context,
'note-at-conn-end-tip',
kOptionAllowAskForNoteAtEndOfConnection,
isServer: false,
));
return _Card(title: 'Other', children: children); return _Card(title: 'Other', children: children);
} }
@@ -1757,21 +1763,23 @@ class _DisplayState extends State<_Display> {
groupValue: groupValue, groupValue: groupValue,
label: 'Scrollbar', label: 'Scrollbar',
onChanged: isOptFixed ? null : onChanged), onChanged: isOptFixed ? null : onChanged),
_Radio(context, if (!isWeb) ...[
value: kRemoteScrollStyleEdge, _Radio(context,
groupValue: groupValue, value: kRemoteScrollStyleEdge,
label: 'ScrollEdge', groupValue: groupValue,
onChanged: isOptFixed ? null : onChanged), label: 'ScrollEdge',
Offstage( onChanged: isOptFixed ? null : onChanged),
offstage: groupValue != kRemoteScrollStyleEdge, Offstage(
child: EdgeThicknessControl( offstage: groupValue != kRemoteScrollStyleEdge,
value: double.tryParse(bind.mainGetUserDefaultOption( child: EdgeThicknessControl(
key: kOptionEdgeScrollEdgeThickness)) ?? value: double.tryParse(bind.mainGetUserDefaultOption(
100.0, key: kOptionEdgeScrollEdgeThickness)) ??
onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness) 100.0,
? null onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness)
: onEdgeScrollEdgeThicknessChanged, ? null
)), : onEdgeScrollEdgeThicknessChanged,
)),
],
]); ]);
} }

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart'; 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:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -52,7 +53,7 @@ enum MouseFocusScope {
} }
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
const FileManagerPage( FileManagerPage(
{Key? key, {Key? key,
required this.id, required this.id,
required this.password, required this.password,
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
final bool? forceRelay; final bool? forceRelay;
final String? connToken; final String? connToken;
final DesktopTabController? tabController; final DesktopTabController? tabController;
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() {
final state = _FileManagerPageState();
_lastState.value = state;
return state;
}
} }
class _FileManagerPageState extends State<FileManagerPage> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Overlay(key: _overlayKeyState.key, initialEntries: [ return Overlay(key: _overlayKeyState.key, initialEntries: [
OverlayEntry(builder: (_) { OverlayEntry(builder: (_) {
return Scaffold( return willPopScope(Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Flexible(flex: 2, child: statusList()) 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:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.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/consts.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(params['id']), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: params['id'],
tabController: tabController,
)) {
return;
}
tabController.closeBy(params['id']);
},
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(params['id']), key: ValueKey(params['id']),
id: params['id'], id: params['id'],
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length; 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) { if (connLength <= 1) {
tabController.clear(); tabController.clear();
return true; return true;

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import 'package:xterm/xterm.dart';
import 'terminal_connection_manager.dart'; import 'terminal_connection_manager.dart';
class TerminalPage extends StatefulWidget { class TerminalPage extends StatefulWidget {
const TerminalPage({ TerminalPage({
Key? key, Key? key,
required this.id, required this.id,
required this.password, required this.password,
@@ -25,9 +25,16 @@ class TerminalPage extends StatefulWidget {
final bool? isSharedPassword; final bool? isSharedPassword;
final String? connToken; final String? connToken;
final int terminalId; final int terminalId;
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
@override @override
State<TerminalPage> createState() => _TerminalPageState(); State<TerminalPage> createState() {
final state = _TerminalPageState();
_lastState.value = state;
return state;
}
} }
class _TerminalPageState extends State<TerminalPage> class _TerminalPageState extends State<TerminalPage>
@@ -59,12 +66,13 @@ class _TerminalPageState extends State<TerminalPage>
// Initialize terminal connection // Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id); widget.tabController.onSelected?.call(widget.id);
// Check if this is a new connection or additional terminal // 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 // 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.getTerminalCount(widget.id) > 1; TerminalConnectionManager.hasConnection(widget.id) &&
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
if (!isExistingConnection) { if (!isExistingConnection) {
// First terminal - show loading dialog, wait for onReady // First terminal - show loading dialog, wait for onReady
_ffi.dialogManager _ffi.dialogManager

View File

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

View File

@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.build(context); super.build(context);
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager); clientClose(sessionId, _ffi);
return false; return false;
}, },
child: MultiProvider(providers: [ 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/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.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/consts.dart';
import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: peerId!, label: peerId!,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: peerId!,
tabController: tabController,
)) {
return;
}
tabController.closeBy(peerId!);
},
page: ViewCameraPage( page: ViewCameraPage(
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId!, id: peerId!,
@@ -287,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
translate('Close'), translate('Close'),
style: style, style: style,
), ),
proc: () { proc: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: key,
tabController: tabController,
)) {
return;
}
tabController.closeBy(key); tabController.closeBy(key);
cancelFunc(); cancelFunc();
}, },
@@ -340,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length; final connLength = tabController.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) { if (connLength <= 1) {
tabController.clear(); tabController.clear();
return true; return true;
@@ -393,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: ViewCameraPage( page: ViewCameraPage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,

View File

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

View File

@@ -12,7 +12,11 @@ import '../../common/widgets/dialog.dart';
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
FileManagerPage( 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); : super(key: key);
final String id; final String id;
final String? password; final String? password;
@@ -113,8 +117,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
leading: Row(children: [ leading: Row(children: [
IconButton( IconButton(
icon: Icon(Icons.close), icon: Icon(Icons.close),
onPressed: () => onPressed: () => clientClose(gFFI.sessionId, gFFI)),
clientClose(gFFI.sessionId, gFFI.dialogManager)),
]), ]),
centerTitle: true, centerTitle: true,
title: ToggleSwitch( title: ToggleSwitch(
@@ -591,67 +594,67 @@ class _FileManagerViewState extends State<FileManagerView> {
Widget headTools() => Container( Widget headTools() => Container(
child: Row( 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: [ children: [
Expanded(child: Obx(() { IconButton(
final home = controller.options.value.home; icon: Icon(Icons.arrow_back),
final isWindows = controller.options.value.isWindows; onPressed: controller.goBack,
return BreadCrumb( ),
items: getPathBreadCrumbItems(controller.shortPath, isWindows, IconButton(
() => controller.goToHomeDirectory(), (list) { icon: Icon(Icons.arrow_upward),
var path = ""; onPressed: controller.goToParentDirectory,
if (home.startsWith(list[0])) { ),
// absolute path PopupMenuButton<SortBy>(
for (var item in list) { tooltip: "",
path = PathUtil.join(path, item, isWindows); 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 { } else {
path += home; ascending.value = true;
for (var item in list) {
path = PathUtil.join(path, item, isWindows);
}
} }
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<SortBy>(
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( Widget listTail() => Obx(() => Container(
height: 100, height: 100,

View File

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

View File

@@ -98,6 +98,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _disableUdp = false; var _disableUdp = false;
var _enableIpv6Punch = false; var _enableIpv6Punch = false;
var _isUsingPublicServer = false; var _isUsingPublicServer = false;
var _allowAskForNoteAtEndOfConnection = false;
_SettingsState() { _SettingsState() {
_enableAbr = option2bool( _enableAbr = option2bool(
@@ -136,6 +137,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices); _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch); _enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); _enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
_allowAskForNoteAtEndOfConnection =
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
} }
@override @override
@@ -782,6 +785,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onPressed: (context) { onPressed: (context) {
showThemeSettings(gFFI.dialogManager); 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) if (isAndroid)

View File

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

View File

@@ -197,7 +197,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
clientClose(sessionId, gFFI.dialogManager); clientClose(sessionId, gFFI);
return false; return false;
}, },
child: Scaffold( child: Scaffold(
@@ -310,7 +310,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
color: Colors.white, color: Colors.white,
icon: Icon(Icons.clear), icon: Icon(Icons.clear),
onPressed: () { onPressed: () {
clientClose(sessionId, gFFI.dialogManager); clientClose(sessionId, gFFI);
}, },
), ),
IconButton( 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/plugin/widgets/desc_ui.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.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:tuple/tuple.dart';
import 'package:image/image.dart' as img2; import 'package:image/image.dart' as img2;
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@@ -933,11 +934,21 @@ class FfiModel with ChangeNotifier {
/// Show a message box with [type], [title] and [text]. /// Show a message box with [type], [title] and [text].
showMsgBox(SessionID sessionId, String type, String title, String text, showMsgBox(SessionID sessionId, String type, String title, String text,
String link, bool hasRetry, OverlayDialogManager dialogManager, String link, bool hasRetry, OverlayDialogManager dialogManager,
{bool? hasCancel}) { {bool? hasCancel}) async {
msgBox(sessionId, type, title, text, link, dialogManager, final showNoteEdit = parent.target != null &&
hasCancel: hasCancel, allowAskForNoteAtEndOfConnection(parent.target, false) &&
reconnect: hasRetry ? reconnect : null, (title == "Connection Error" || type == "restarting") &&
reconnectTimeout: hasRetry ? _reconnects : null); !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(); _timer?.cancel();
if (hasRetry) { if (hasRetry) {
_timer = Timer(Duration(seconds: _reconnects), () { _timer = Timer(Duration(seconds: _reconnects), () {
@@ -958,8 +969,30 @@ class FfiModel with ChangeNotifier {
onCancel: closeConnection); onCancel: closeConnection);
} }
void showRelayHintDialog(SessionID sessionId, String type, String title, Future<void> showRelayHintDialog(
String text, OverlayDialogManager dialogManager, String peerId) { 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) { dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
onClose() { onClose() {
closeConnection(); closeConnection();
@@ -968,13 +1001,10 @@ class FfiModel with ChangeNotifier {
final style = final style =
ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
var hint = "\n\n${translate('relay_hint_tip')}";
if (text.contains("10054") || text.contains("104")) {
hint = "";
}
return CustomAlertDialog( return CustomAlertDialog(
title: null, title: null,
content: msgboxContent(type, title, "${translate(text)}$hint"), content: msgboxContent(type, title, text2),
actions: [ actions: [
dialogButton('Close', onPressed: onClose, isOutline: true), dialogButton('Close', onPressed: onClose, isOutline: true),
if (type == 'relay-hint') 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]. /// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async { handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_queryAuditGuid(peerId);
// This call is to ensuer the keyboard mode is updated depending on the peer version. // This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode(); parent.target?.inputModel.updateKeyboardMode();
@@ -2096,9 +2207,8 @@ class CanvasModel with ChangeNotifier {
Future<void> updateScrollStyle() async { Future<void> updateScrollStyle() async {
final style = await bind.sessionGetScrollStyle(sessionId: sessionId); final style = await bind.sessionGetScrollStyle(sessionId: sessionId);
_scrollStyle = style != null _scrollStyle =
? ScrollStyle.fromString(style) style != null ? ScrollStyle.fromString(style) : ScrollStyle.scrollauto;
: ScrollStyle.scrollauto;
if (_scrollStyle != ScrollStyle.scrollauto) { if (_scrollStyle != ScrollStyle.scrollauto) {
_resetScroll(); _resetScroll();
@@ -2108,7 +2218,8 @@ class CanvasModel with ChangeNotifier {
} }
Future<void> initializeEdgeScrollEdgeThickness() async { Future<void> initializeEdgeScrollEdgeThickness() async {
final savedValue = await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId); final savedValue =
await bind.sessionGetEdgeScrollEdgeThickness(sessionId: sessionId);
if (savedValue != null) { if (savedValue != null) {
_edgeScrollEdgeThickness = savedValue; _edgeScrollEdgeThickness = savedValue;
@@ -2223,12 +2334,12 @@ class CanvasModel with ChangeNotifier {
(Vector2, Vector2) getScrollInfo() { (Vector2, Vector2) getScrollInfo() {
final scrollPixel = Vector2( final scrollPixel = Vector2(
_horizontal.hasClients ? _horizontal.position.pixels : 0, _horizontal.hasClients ? _horizontal.position.pixels : 0,
_vertical.hasClients ? _vertical.position.pixels : 0); _vertical.hasClients ? _vertical.position.pixels : 0);
final max = Vector2( final max = Vector2(
_horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0, _horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0,
_vertical.hasClients ? _vertical.position.maxScrollExtent : 0); _vertical.hasClients ? _vertical.position.maxScrollExtent : 0);
return (scrollPixel, max); return (scrollPixel, max);
} }
@@ -3310,7 +3421,6 @@ class FFI {
var version = ''; var version = '';
var connType = ConnType.defaultConn; var connType = ConnType.defaultConn;
var closed = false; var closed = false;
var auditNote = '';
/// dialogManager use late to ensure init after main page binding [globalKey] /// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager(); late final dialogManager = OverlayDialogManager();
@@ -3401,7 +3511,6 @@ class FFI {
List<int>? displays, List<int>? displays,
}) { }) {
closed = false; closed = false;
auditNote = '';
if (isMobile) mobileReset(); if (isMobile) mobileReset();
assert( assert(
(!(isPortForward && isViewCamera)) && (!(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() {} void dispose() {}
} }

View File

@@ -2103,6 +2103,26 @@ pub mod sessions {
s 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( fn check_remove_unused_displays(
current: Option<usize>, current: Option<usize>,
session_id: &SessionID, session_id: &SessionID,

View File

@@ -254,6 +254,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn<b
SyncReturn(v) 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) { pub fn session_close(session_id: SessionID) {
if let Some(session) = sessions::remove_session_by_session_id(&session_id) { if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
// `release_remote_keys` is not required for mobile platforms in common cases. // `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 { pub fn session_alternative_codecs(session_id: SessionID) -> String {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let (vp8, av1, h264, h265) = session.alternative_codecs(); 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", ""),
("disable-udp-tip", ""), ("disable-udp-tip", ""),
("server-oss-not-support-tip", ""), ("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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