diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 832b96d24..adf7b1d45 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -142,6 +142,10 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse"; const String kOptionCodecPreference = "codec-preference"; const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; +const String kOptionRemoteMenubarEdge = "remote-menubar-edge"; +const String kOptionRemoteMenubarFraction = "remote-menubar-frac"; +const String kOptionAllowMultiEdgeToolbarDock = + "allow-multi-edge-toolbar-dock"; const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; const String kOptionRemoteMenubarState = "remoteMenubarState"; const String kOptionPeerSorting = "peer-sorting"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 2841c1d27..d1d620014 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -488,6 +488,16 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), + if (!bind.isIncomingOnly()) + _OptionCheckBox( + context, + 'allow-remote-toolbar-docking-any-edge', + kOptionAllowMultiEdgeToolbarDock, + isServer: false, + update: (_) { + reloadAllWindows(); + }, + ), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), if (!isWeb) wallpaper(), if (!isWeb && !bind.isIncomingOnly()) ...[ diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 645cbe1cb..44a2dc1c7 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -28,6 +28,220 @@ import './kb_layout_type_chooser.dart'; import 'package:flutter_hbb/utils/scale.dart'; import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; +enum _ToolbarEdge { top, right, bottom, left } + +_ToolbarEdge _parseToolbarEdge(String? s) { + switch (s) { + case 'right': + return _ToolbarEdge.right; + case 'bottom': + return _ToolbarEdge.bottom; + case 'left': + return _ToolbarEdge.left; + default: + return _ToolbarEdge.top; + } +} + +String _toolbarEdgeToString(_ToolbarEdge e) { + switch (e) { + case _ToolbarEdge.top: + return 'top'; + case _ToolbarEdge.right: + return 'right'; + case _ToolbarEdge.bottom: + return 'bottom'; + case _ToolbarEdge.left: + return 'left'; + } +} + +bool _isHorizontalEdge(_ToolbarEdge e) => + e == _ToolbarEdge.top || e == _ToolbarEdge.bottom; + +const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x'; + +double _clampToolbarFraction(double fraction, double left, double right) { + if (fraction < left) fraction = left; + if (fraction > right) fraction = right; + return fraction; +} + +Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) { + final isHorizontal = _isHorizontalEdge(edge); + final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360); + final size = measured ?? fallback; + final long = size.longestSide; + final short = size.shortestSide; + return Size(isHorizontal ? long : short, isHorizontal ? short : long); +} + +Offset _toolbarOffsetForEdge({ + required _ToolbarEdge edge, + required double fraction, + required Size parentSize, + required Size toolbarSize, +}) { + final xTravel = parentSize.width - toolbarSize.width; + final yTravel = parentSize.height - toolbarSize.height; + switch (edge) { + case _ToolbarEdge.top: + return Offset(xTravel * fraction, 0); + case _ToolbarEdge.bottom: + return Offset(xTravel * fraction, yTravel); + case _ToolbarEdge.left: + return Offset(0, yTravel * fraction); + case _ToolbarEdge.right: + return Offset(xTravel, yTravel * fraction); + } +} + +double _fractionForAlignedDrag({ + required double cursor, + required double grabOffset, + required double parentExtent, + required double toolbarExtent, + required double left, + required double right, +}) { + final travelExtent = parentExtent - toolbarExtent; + if (travelExtent <= 0) { + return _clampToolbarFraction(0.5, left, right); + } + return _clampToolbarFraction( + (cursor - grabOffset) / travelExtent, left, right); +} + +({double left, double right}) _fractionBoundsForEdge( + _ToolbarEdge edge, + double left, + double right, +) { + return _isHorizontalEdge(edge) + ? (left: left, right: right) + : (left: 0, right: 1); +} + +String _toolbarRawFraction({ + required bool multiEdgeEnabled, + required _ToolbarEdge edge, + required String? savedFraction, + required String? legacyFraction, +}) { + if (!multiEdgeEnabled) { + return (legacyFraction != null && legacyFraction.isNotEmpty) + ? legacyFraction + : '0.5'; + } + if (savedFraction != null && savedFraction.isNotEmpty) { + return savedFraction; + } + if (edge == _ToolbarEdge.top && + legacyFraction != null && + legacyFraction.isNotEmpty) { + return legacyFraction; + } + return '0.5'; +} + +// Returns the alignment for the wrapper Align that positions the entire +// toolbar against the given edge at the given fraction along that edge. +// Alignment uses [-1, 1] coordinates (0 = center). +Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) { + final f = fraction * 2 - 1; + switch (edge) { + case _ToolbarEdge.top: + return Alignment(f, -1); + case _ToolbarEdge.bottom: + return Alignment(f, 1); + case _ToolbarEdge.left: + return Alignment(-1, f); + case _ToolbarEdge.right: + return Alignment(1, f); + } +} + +// The drag handle hangs off the side of the toolbar facing away from the +// docked edge, so the icons themselves sit flush against that edge. +BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) { + const r = Radius.circular(5); + switch (edge) { + case _ToolbarEdge.top: + return const BorderRadius.vertical(bottom: r); + case _ToolbarEdge.bottom: + return const BorderRadius.vertical(top: r); + case _ToolbarEdge.left: + return const BorderRadius.horizontal(right: r); + case _ToolbarEdge.right: + return const BorderRadius.horizontal(left: r); + } +} + +int _monitorMenuQuarterTurns(_ToolbarEdge edge) { + switch (edge) { + case _ToolbarEdge.left: + return 1; + case _ToolbarEdge.right: + return 3; + case _ToolbarEdge.top: + case _ToolbarEdge.bottom: + return 0; + } +} + +IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) { + switch (edge) { + case _ToolbarEdge.top: + return isCollapsed ? Icons.expand_more : Icons.expand_less; + case _ToolbarEdge.bottom: + return isCollapsed ? Icons.expand_less : Icons.expand_more; + case _ToolbarEdge.left: + return isCollapsed ? Icons.chevron_right : Icons.chevron_left; + case _ToolbarEdge.right: + return isCollapsed ? Icons.chevron_left : Icons.chevron_right; + } +} + +class _ToolbarDockingOptions { + _ToolbarDockingOptions({ + required this.edge, + required this.fraction, + required this.multiEdgeEnabled, + }); + + _ToolbarEdge edge; + double fraction; + bool multiEdgeEnabled; +} + +final _toolbarDockingOptionsBySession = {}; + +String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString(); + +_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) => + _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)]; + +void _cacheToolbarDockingOptions({ + required SessionID sessionId, + required _ToolbarEdge edge, + required double fraction, + required bool multiEdgeEnabled, +}) { + final key = _toolbarDockingCacheKey(sessionId); + final cached = _toolbarDockingOptionsBySession[key]; + if (cached == null) { + _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions( + edge: edge, + fraction: fraction, + multiEdgeEnabled: multiEdgeEnabled, + ); + return; + } + cached.edge = edge; + cached.fraction = fraction; + cached.multiEdgeEnabled = multiEdgeEnabled; +} + class ToolbarState { late RxBool _pin; @@ -250,8 +464,26 @@ class RemoteToolbar extends StatefulWidget { class _RemoteToolbarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - final _fractionX = 0.5.obs; + final _fraction = 0.5.obs; + final _edge = _ToolbarEdge.top.obs; final _dragging = false.obs; + // Live drag preview: where the toolbar would dock if the user dropped now. + final _previewEdge = Rxn<_ToolbarEdge>(); + final _previewFraction = Rxn(); + // Measured size of the live toolbar, so the preview ghost matches reality + // (collapsed handle vs expanded toolbar). Updated after every layout pass. + final _toolbarSize = Rxn(); + final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root'); + // When false (default), the toolbar stays on the top edge and the drag + // handle just slides it horizontally — preserving long-standing UX while + // still fixing the bug where dragging only moved the handle. When true, + // the user has opted into multi-edge docking with nearest-edge snap. + // Kept in sync after settings-triggered rebuilds. + final _multiEdgeEnabled = false.obs; + final _dockingOptionsInitialized = false.obs; + bool _pendingDockingOptionSync = false; + int _dockingOptionSyncSerial = 0; + int _dragEpoch = 0; int get windowId => stateGlobal.windowId; @@ -273,16 +505,144 @@ class _RemoteToolbarState extends State { void _minimize() async => await WindowController.fromWindowId(windowId).minimize(); + Future _syncDockingOptions({required bool force}) async { + final syncSerial = ++_dockingOptionSyncSerial; + if (_dragging.isTrue) { + _deferDockingOptionsSync(); + return; + } + final dragEpoch = _dragEpoch; + + // Use the canonical helper so the option's documented default semantics + // apply (allow-* prefix => default false). Keeping it raw-string would + // diverge from how _OptionCheckBox displays the same key. + final multiEdgeEnabled = + mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + if (cached == null && pi.isSet.isFalse) { + return; + } + final hadDockingOptions = cached != null; + final wasMultiEdgeEnabled = + cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value; + if (!force && + hadDockingOptions && + wasMultiEdgeEnabled == multiEdgeEnabled) { + _pendingDockingOptionSync = false; + return; + } + + final savedFraction = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction); + // Backward compat: legacy horizontal-only position. + final legacyFraction = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + + var nextEdge = _edge.value; + var savedFractionForNextEdge = savedFraction; + var keepCurrentPosition = false; + if (!multiEdgeEnabled) { + nextEdge = _ToolbarEdge.top; + } else if (force || wasMultiEdgeEnabled || cached == null) { + final edgeStr = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + nextEdge = _parseToolbarEdge(edgeStr); + } else { + // The setting changed from top-only to multi-edge while this toolbar is + // already visible. Keep its current position instead of jumping to the + // last saved multi-edge dock. + nextEdge = cached.edge; + savedFractionForNextEdge = cached.fraction.toString(); + keepCurrentPosition = true; + } + + final rawFraction = _toolbarRawFraction( + multiEdgeEnabled: multiEdgeEnabled, + edge: nextEdge, + savedFraction: savedFractionForNextEdge, + legacyFraction: legacyFraction, + ); + // Clamp to the saved drag-bound contract so a corrupted or out-of-range + // saved value can't bypass it until the user drags again. + final dragLeft = double.tryParse( + bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ?? + 0.0; + final dragRight = double.tryParse( + bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ?? + 1.0; + final fractionBounds = + _fractionBoundsForEdge(nextEdge, dragLeft, dragRight); + final nextFraction = (double.tryParse(rawFraction) ?? 0.5) + .clamp(fractionBounds.left, fractionBounds.right) + .toDouble(); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + if (_dragging.isTrue || dragEpoch != _dragEpoch) { + _deferDockingOptionsSync(); + return; + } + _edge.value = nextEdge; + _fraction.value = nextFraction; + _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + _cacheToolbarDockingOptions( + sessionId: widget.ffi.sessionId, + edge: nextEdge, + fraction: nextFraction, + multiEdgeEnabled: multiEdgeEnabled, + ); + _pendingDockingOptionSync = false; + if (!multiEdgeEnabled || keepCurrentPosition) { + bind.sessionPeerOption( + sessionId: widget.ffi.sessionId, + name: kOptionRemoteMenubarEdge, + value: _toolbarEdgeToString(nextEdge), + ); + bind.sessionPeerOption( + sessionId: widget.ffi.sessionId, + name: kOptionRemoteMenubarFraction, + value: nextFraction.toString(), + ); + } + } + + void _deferDockingOptionsSync() { + _pendingDockingOptionSync = true; + if (_dragging.isFalse) { + _syncDockingOptionsAfterDragIfNeeded(); + } + } + + void _markToolbarDragEpoch() { + ++_dragEpoch; + } + + void _syncDockingOptionsAfterDragIfNeeded() { + if (!_pendingDockingOptionSync) return; + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _syncDockingOptions(force: false); + }); + } + @override initState() { super.initState(); + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + final multiEdgeEnabled = + mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); + final shouldResetToTop = + cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled; + if (cached != null && !shouldResetToTop) { + _edge.value = cached.edge; + _fraction.value = cached.fraction; + _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + } + WidgetsBinding.instance.addPostFrameCallback((_) async { - _fractionX.value = double.tryParse(await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, - arg: 'remote-menubar-drag-x') ?? - '0.5') ?? - 0.5; + await _syncDockingOptions(force: cached == null || shouldResetToTop); // Initialize toolbar states (collapse, hide) from session options widget.state.init(widget.ffi.sessionId); }); @@ -303,6 +663,14 @@ class _RemoteToolbarState extends State { }); } + @override + void didUpdateWidget(covariant RemoteToolbar oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _syncDockingOptions(force: false); + }); + } + _debouncerHideProc(int v) { if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { collapse.value = true; @@ -311,64 +679,130 @@ class _RemoteToolbarState extends State { @override dispose() { - super.dispose(); - + ++_dockingOptionSyncSerial; widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); + super.dispose(); } @override Widget build(BuildContext context) { return Obx(() { // Wait for initialization to complete to prevent flickering - if (!widget.state.initialized.value) { + if (!widget.state.initialized.value || + !_dockingOptionsInitialized.value) { return const SizedBox.shrink(); } // If toolbar is hidden, return empty widget if (hide.value) { return const SizedBox.shrink(); } - return Align( - alignment: Alignment.topCenter, - child: collapse.isFalse - ? _buildToolbar(context) - : _buildDraggableCollapse(context), + final edge = _edge.value; + final isHorizontal = _isHorizontalEdge(edge); + + // Measure the live toolbar after every layout so the preview ghost can + // match its actual footprint (collapsed handle vs expanded toolbar). + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_dragging.isTrue) return; + final ro = _toolbarKey.currentContext?.findRenderObject(); + if (ro is RenderBox && ro.hasSize) { + final s = ro.size; + if (_toolbarSize.value != s) _toolbarSize.value = s; + } + }); + + final toolbar = Align( + alignment: _alignmentForEdge(edge, _fraction.value), + child: KeyedSubtree( + key: _toolbarKey, + child: collapse.isFalse + ? _buildToolbar(context, edge, isHorizontal) + : _buildDraggableCollapse(context, edge, isHorizontal), + ), + ); + + // Always return the Stack — even when not dragging — so the toolbar's + // position in the Element tree stays stable. Wrapping/unwrapping it + // mid-drag was killing the Draggable's gesture state. + return Stack( + fit: StackFit.expand, + children: [ + IgnorePointer( + child: Obx(() { + final pe = _previewEdge.value; + final pf = _previewFraction.value; + if (!_dragging.isTrue || pe == null || pf == null) { + return const SizedBox.shrink(); + } + return _buildDragPreview(context, pe, pf, _toolbarSize.value); + }), + ), + toolbar, + ], ); }); } - Widget _buildDraggableCollapse(BuildContext context) { + Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge, + double fraction, Size? measured) { + final color = Theme.of(context).colorScheme.primary; + // Use the measured live toolbar size so collapsed vs expanded looks + // right. The current orientation may differ from the preview orientation + // (e.g. dragging a top-docked toolbar toward the left edge), so swap the + // long/short axes when previewing a different orientation. + final previewSize = _toolbarSizeForEdge(edge, measured); + return Align( + alignment: _alignmentForEdge(edge, fraction), + child: Container( + width: previewSize.width, + height: previewSize.height, + decoration: BoxDecoration( + color: color.withOpacity(0.10), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.withOpacity(0.55), width: 1.5), + ), + ), + ); + } + + Widget _buildDraggableCollapse( + BuildContext context, _ToolbarEdge edge, bool isHorizontal) { return Obx(() { if (collapse.isFalse && _dragging.isFalse) { triggerAutoHide(); } - final borderRadius = BorderRadius.vertical( - bottom: Radius.circular(5), - ); - return Align( - alignment: FractionalOffset(_fractionX.value, 0), - child: Offstage( - offstage: _dragging.isTrue, - child: Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, + final borderRadius = _collapseHandleBorderRadius(edge); + return Offstage( + offstage: _dragging.isTrue, + child: Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: borderRadius, + child: _DraggableShowHide( + id: widget.id, + sessionId: widget.ffi.sessionId, + dragging: _dragging, + fraction: _fraction, + edge: _edge, + previewEdge: _previewEdge, + previewFraction: _previewFraction, + toolbarSize: _toolbarSize, + markDragEpoch: _markToolbarDragEpoch, + syncDockingOptionsAfterDragIfNeeded: + _syncDockingOptionsAfterDragIfNeeded, + isHorizontal: isHorizontal, + multiEdgeEnabled: _multiEdgeEnabled.value, + toolbarState: widget.state, + setFullscreen: _setFullscreen, + setMinimize: _minimize, borderRadius: borderRadius, - child: _DraggableShowHide( - id: widget.id, - sessionId: widget.ffi.sessionId, - dragging: _dragging, - fractionX: _fractionX, - toolbarState: widget.state, - setFullscreen: _setFullscreen, - setMinimize: _minimize, - borderRadius: borderRadius, - ), ), ), ); }); } - Widget _buildToolbar(BuildContext context) { + Widget _buildToolbar( + BuildContext context, _ToolbarEdge edge, bool isHorizontal) { final List toolbarItems = []; toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { @@ -382,6 +816,7 @@ class _RemoteToolbarState extends State { return _MonitorMenu( id: widget.id, ffi: widget.ffi, + edge: edge, setRemoteState: widget.setRemoteState); } else { return Offstage(); @@ -407,37 +842,53 @@ class _RemoteToolbarState extends State { if (!isWeb) toolbarItems.add(_RecordMenu()); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: toolbarBorderRadius, - color: Theme.of(context) - .menuBarTheme - .style - ?.backgroundColor - ?.resolve(MaterialState.values.toSet()), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Theme( - data: themeData(), - child: _ToolbarTheme.borderWrapper( - context, - Row( - children: [ - SizedBox(width: _ToolbarTheme.buttonHMargin * 2), - ...toolbarItems, - SizedBox(width: _ToolbarTheme.buttonHMargin * 2) - ], - ), - toolbarBorderRadius), - ), - ), + // innerAxis: how the toolbar icons themselves flow. + // outerAxis: how the toolbar block and the handle stack against each other + // (perpendicular to the dock edge, so the handle hangs off the interior face). + final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical; + final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal; + final spacer = isHorizontal + ? SizedBox(width: _ToolbarTheme.buttonHMargin * 2) + : SizedBox(height: _ToolbarTheme.buttonHMargin * 2); + final toolbarMaterial = Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: toolbarBorderRadius, + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + child: SingleChildScrollView( + scrollDirection: innerAxis, + child: Theme( + data: themeData(), + child: _ToolbarTheme.borderWrapper( + context, + Flex( + direction: innerAxis, + mainAxisSize: MainAxisSize.min, + children: [ + spacer, + ...toolbarItems, + spacer, + ], + ), + toolbarBorderRadius), ), - _buildDraggableCollapse(context), - ], + ), + ); + final handle = _buildDraggableCollapse(context, edge, isHorizontal); + // The handle hangs off the interior face of the toolbar (away from the + // docked edge), centered along that face by the Flex's default cross-axis + // alignment, so the icons themselves sit flush against the docked edge. + final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left) + ? [toolbarMaterial, handle] + : [handle, toolbarMaterial]; + return Flex( + direction: outerAxis, + mainAxisSize: MainAxisSize.min, + children: children, ); } @@ -516,11 +967,13 @@ class _MobileActionMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget { final String id; final FFI ffi; + final _ToolbarEdge edge; final Function(VoidCallback) setRemoteState; const _MonitorMenu({ Key? key, required this.id, required this.ffi, + required this.edge, required this.setRemoteState, }) : super(key: key); @@ -531,9 +984,17 @@ class _MonitorMenu extends StatelessWidget { !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay; @override - Widget build(BuildContext context) => showMonitorsToolbar - ? buildMultiMonitorMenu(context) - : Obx(() => buildMonitorMenu(context)); + Widget build(BuildContext context) { + final child = showMonitorsToolbar + ? buildMultiMonitorMenu(context) + : Obx(() => buildMonitorMenu(context)); + final quarterTurns = _monitorMenuQuarterTurns(edge); + if (quarterTurns == 0) return child; + return RotatedBox( + quarterTurns: quarterTurns, + child: child, + ); + } Widget buildMonitorMenu(BuildContext context) { final width = SimpleWrapper(0); @@ -665,7 +1126,8 @@ class _MonitorMenu extends StatelessWidget { } final scale = _ToolbarTheme.buttonSize / rect.height * 0.75; - final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5; + final height = rect.height * scale; + final startY = (_ToolbarTheme.buttonSize - height) * 0.5; final startX = startY; final children = []; @@ -708,7 +1170,7 @@ class _MonitorMenu extends StatelessWidget { width.value = rect.width * scale + startX * 2; return SizedBox( width: width.value, - height: rect.height * scale + startY * 2, + height: height + startY * 2, child: Stack( children: children, ), @@ -2519,7 +2981,18 @@ class RdoMenuButton extends StatelessWidget { class _DraggableShowHide extends StatefulWidget { final String id; final SessionID sessionId; - final RxDouble fractionX; + final RxDouble fraction; + final Rx<_ToolbarEdge> edge; + final Rxn<_ToolbarEdge> previewEdge; + final Rxn previewFraction; + final Rxn toolbarSize; + final VoidCallback markDragEpoch; + final VoidCallback syncDockingOptionsAfterDragIfNeeded; + final bool isHorizontal; + // Whether multi-edge docking is enabled for this session (toggled in + // Settings -> Other). When false, the drag handle slides the toolbar + // horizontally on the top edge and never switches edges. + final bool multiEdgeEnabled; final RxBool dragging; final ToolbarState toolbarState; final BorderRadius borderRadius; @@ -2531,7 +3004,15 @@ class _DraggableShowHide extends StatefulWidget { Key? key, required this.id, required this.sessionId, - required this.fractionX, + required this.fraction, + required this.edge, + required this.previewEdge, + required this.previewFraction, + required this.toolbarSize, + required this.markDragEpoch, + required this.syncDockingOptionsAfterDragIfNeeded, + required this.isHorizontal, + required this.multiEdgeEnabled, required this.dragging, required this.toolbarState, required this.setFullscreen, @@ -2544,10 +3025,12 @@ class _DraggableShowHide extends StatefulWidget { } class _DraggableShowHideState extends State<_DraggableShowHide> { - Offset position = Offset.zero; - Size size = Size.zero; double left = 0.0; double right = 1.0; + Offset? _lastPointerDown; + Offset? _dragGrabOffset; + double? _dragLongAxisGrabOffset; + Size? _dragToolbarSize; RxBool get collapse => widget.toolbarState.collapse; @@ -2573,41 +3056,174 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { } } + // Bias applied to the currently-previewed edge so a drag hovering between + // two edges doesn't flicker. Only relevant when multi-edge is enabled. + static const double _switchHysteresisPx = 50.0; + + _ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) { + if (!widget.multiEdgeEnabled) return widget.edge.value; + + double rawDist(_ToolbarEdge e) { + switch (e) { + case _ToolbarEdge.top: + return cursor.dy; + case _ToolbarEdge.bottom: + return mediaSize.height - cursor.dy; + case _ToolbarEdge.left: + return cursor.dx; + case _ToolbarEdge.right: + return mediaSize.width - cursor.dx; + } + } + + final previewed = widget.previewEdge.value; + var winner = widget.edge.value; + var best = double.infinity; + for (final e in _ToolbarEdge.values) { + final biased = + e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e); + if (biased < best) { + best = biased; + winner = e; + } + } + return winner; + } + + void _ensureDragGrabOffset(Offset cursor) { + if (_dragGrabOffset != null) return; + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + final toolbarSize = + _toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value); + _dragToolbarSize = toolbarSize; + final toolbarOffset = _toolbarOffsetForEdge( + edge: widget.edge.value, + fraction: widget.fraction.value, + parentSize: mediaSize, + toolbarSize: toolbarSize, + ); + _dragGrabOffset = cursor - toolbarOffset; + _dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value) + ? _dragGrabOffset?.dx + : _dragGrabOffset?.dy; + } + + double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) { + final offset = _dragLongAxisGrabOffset ?? 0; + final extent = + _isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height; + return _clampToolbarFraction(offset, 0, extent); + } + + void _updatePreview(Offset cursor) { + _ensureDragGrabOffset(cursor); + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + final winner = _nearestToolbarEdge(cursor, mediaSize); + widget.previewEdge.value = winner; + + final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize); + final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize); + final double frac; + if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) { + frac = _fractionForAlignedDrag( + cursor: cursor.dx, + grabOffset: grabOffset, + parentExtent: mediaSize.width, + toolbarExtent: toolbarSize.width, + left: left, + right: right, + ); + } else { + final fractionBounds = _fractionBoundsForEdge(winner, left, right); + frac = _fractionForAlignedDrag( + cursor: cursor.dy, + grabOffset: grabOffset, + parentExtent: mediaSize.height, + toolbarExtent: toolbarSize.height, + left: fractionBounds.left, + right: fractionBounds.right, + ); + } + widget.previewFraction.value = frac; + } + + void _resetDragTracking() { + _lastPointerDown = null; + _dragGrabOffset = null; + _dragLongAxisGrabOffset = null; + _dragToolbarSize = null; + } + + void _commitPreview() { + final newEdge = widget.previewEdge.value; + final frac = widget.previewFraction.value; + widget.previewEdge.value = null; + widget.previewFraction.value = null; + widget.dragging.value = false; + widget.markDragEpoch(); + _resetDragTracking(); + widget.syncDockingOptionsAfterDragIfNeeded(); + if (newEdge == null || frac == null) return; + widget.edge.value = newEdge; + widget.fraction.value = frac; + _cacheToolbarDockingOptions( + sessionId: widget.sessionId, + edge: newEdge, + fraction: frac, + multiEdgeEnabled: widget.multiEdgeEnabled, + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarEdge, + value: _toolbarEdgeToString(newEdge), + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarFraction, + value: frac.toString(), + ); + if (widget.multiEdgeEnabled) { + return; + } + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: _legacyRemoteMenubarDragX, + value: frac.toString(), + ); + } + Widget _buildDraggable(BuildContext context) { - return Draggable( - axis: Axis.horizontal, - child: Icon( - Icons.drag_indicator, - size: 20, - color: MyTheme.color(context).drag_indicator, + return Listener( + onPointerDown: (event) => _lastPointerDown = event.position, + child: Draggable( + // When multi-edge docking is off the toolbar stays on the top edge, + // so lock the feedback to horizontal motion — otherwise the handle + // floats away from the top while dragging and the toolbar looks + // unmoored. When multi-edge is on we need 2D drag for snap-to-edge. + axis: widget.multiEdgeEnabled ? null : Axis.horizontal, + child: Icon( + widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle, + size: 20, + color: MyTheme.color(context).drag_indicator, + ), + feedback: widget, + onDragStarted: () { + widget.markDragEpoch(); + final pointerDown = _lastPointerDown; + if (pointerDown != null) { + _ensureDragGrabOffset(pointerDown); + } + widget.dragging.value = true; + // Seed the preview at the current docked edge/fraction so something + // shows the instant the drag begins, before the first onDragUpdate. + widget.previewEdge.value = widget.edge.value; + widget.previewFraction.value = widget.fraction.value; + }, + onDragUpdate: (details) { + _updatePreview(details.globalPosition); + }, + onDragEnd: (_) => _commitPreview(), ), - feedback: widget, - onDragStarted: (() { - final RenderObject? renderObj = context.findRenderObject(); - if (renderObj != null) { - final RenderBox renderBox = renderObj as RenderBox; - size = renderBox.size; - position = renderBox.localToGlobal(Offset.zero); - } - widget.dragging.value = true; - }), - onDragEnd: (details) { - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - widget.fractionX.value += - (details.offset.dx - position.dx) / (mediaSize.width - size.width); - if (widget.fractionX.value < left) { - widget.fractionX.value = left; - } - if (widget.fractionX.value > right) { - widget.fractionX.value = right; - } - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: 'remote-menubar-drag-x', - value: widget.fractionX.value.toString(), - ); - widget.dragging.value = false; - }, ); } @@ -2637,7 +3253,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } - final child = Row( + final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical; + final child = Flex( + direction: axis, mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), @@ -2678,7 +3296,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { message: translate( collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( - collapse.isFalse ? Icons.expand_less : Icons.expand_more, + _toolbarCollapseIcon(widget.edge.value, collapse.isTrue), size: iconSize, ), ))), @@ -2720,7 +3338,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { borderRadius: widget.borderRadius, ), child: SizedBox( - height: 20, + height: widget.isHorizontal ? 20 : null, + width: widget.isHorizontal ? null : 20, child: child, ), ), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 6fdffd796..984d6a25c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!Platform.isLinux) return; + if (!isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 4113c1391..e13404802 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 1a3260c5a..9f6b69c8b 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 17a89ce07..0aa61b1eb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 799ca951f..2f706cc89 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 1ff10c49d..a90e5e194 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), ("Enable privacy mode", "允许隐私模式"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2b9c6219e..7f50d826f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 7410124df..c9d3b4eb0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 030bc626d..e6233e91e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ("Enable privacy mode", "Datenschutzmodus aktivieren"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 0633889a7..d03bb069c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 73974a2e5..595169b8a 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -274,5 +274,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("password-hidden-tip", "Permanent password is set (hidden)."), ("preset-password-in-use-tip", "Preset password is currently in use."), + ("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 16d43c9b4..131a85fbf 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b822432a0..5e73b58a8 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), ("Enable privacy mode", "Habilitar modo privado"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index a00c312b8..76abc8563 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index aaf8a8be8..9e19d1fea 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d34e4239e..9e01b7eb0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 1bddd39d1..f8283685b 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6f7bb2880..f21d9b0df 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ("Enable privacy mode", "Activer le mode de confidentialité"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index fba2fd83d..2fc8f282d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index 8b8568c85..ac0a588a8 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), + ("Use D3D rendering", ""), ("Printer", "પ્રિન્ટર"), ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), @@ -743,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 682ee0c46..44b940784 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index d35095fd1..904d43118 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "सुलभ डिवाइस"), ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), + ("Use D3D rendering", ""), ("Printer", "प्रिंटर"), ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), @@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "प्रदर्शित नाम"), ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), + ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 505b01df9..0593ff6b7 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b4cbc1f23..3eb16890f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ("Enable privacy mode", "Adatvédelmi mód aktiválása"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index bbd95e79a..bcda0a3a8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 479551fcc..a5132e027 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ("Enable privacy mode", "Abilita modalità privacy"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b55a6664f..2879e86bf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index de68574e1..350d570b0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), ("Enable privacy mode", "개인정보 보호 모드 사용함"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a2a1624f7..4476fadc7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 82422c30a..47ace51ae 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 906d056bd..4f8e1f59f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index 099f1d385..4dcfe9e74 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Use D3D rendering", ""), ("Printer", "പ്രിന്റർ"), ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), @@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), + ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 5795b9eeb..9325dfa1f 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 0f91d6a61..55d272666 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ("Enable privacy mode", "Privacymodus inschakelen"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 972afc170..fdf4ae8c5 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 899c8da71..4138b46e4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 36581d4f1..1428a71d0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "A senha permanente está definida como (oculta)."), ("preset-password-in-use-tip", "A senha predefinida está sendo usada."), ("Enable privacy mode", "Habilitar modo de privacidade"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 45b22684e..bde4a4201 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), ("Check for software update on startup", "Verifică actualizări la pornire"), - ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), ("Filter by intersection", "Filtrează prin intersecție"), ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3917c6fa2..2605582f4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ("Enable privacy mode", "Использовать режим конфиденциальности"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 68ce541f2..06919b752 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6b4e16688..963f48728 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3f35dea88..0f85af0c3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f7f6c16d4..7c965cd45 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index bedbe4856..fc33e4671 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index eda7851c1..664dc4745 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 6e5652560..93aeb6462 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 5e25801d2..33b359c5e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c2d058c98..a24c60bf6 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d93ad4f68..c28086cc9 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), ("Enable privacy mode", "Gizlilik modunu etkinleştir"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b23b84949..6df025303 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 3e1c4f25e..7107bc261 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 3fadb0efc..0910025ed 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); }