Compare commits

..

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f65f5812b2 Fix text color visibility in dark theme dialogs
Add explicit white70 color to dark theme text styles to ensure text is visible against dark dialog backgrounds. This matches the pattern used in light theme where black87 is explicitly set.

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
2026-02-02 08:27:38 +00:00
copilot-swe-agent[bot]
b8f5b91168 Initial plan 2026-02-02 08:22:17 +00:00
Copilot
6306f83316 Fix non-link text color in dialogs with links for dark theme (#14220)
* Initial plan

* Fix dialog text color for dark theme with links

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Keep original link color (blue), only fix non-link text color

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* fix: dialog text color in dark theme

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-02-01 12:18:07 +08:00
XLion
96075fdf49 Update tw.rs (#14138) 2026-01-31 16:38:09 +08:00
Copilot
8c6dcf53a6 iOS terminal: Add touch swipe and floating back button for exit (#14208)
* Initial plan

* Add iOS edge swipe gesture to exit terminal session

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Improve iOS edge swipe gesture with responsive thresholds and better gesture handling

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Fix: Reset _swipeCurrentX in onHorizontalDragStart to prevent stale state

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Add trackpad support documentation for iOS edge swipe gesture

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Add iOS-style circular back button to terminal page

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Remove trackpad support documentation - not needed with back button

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Filter edge swipe gesture to touch-only input (exclude mouse/trackpad)

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* fix: missing import

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ios): terminal swip exit gesture

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update flutter/lib/mobile/pages/terminal_page.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 16:37:45 +08:00
fufesou
e1b1a927b8 fix(ios): capsLock, workaround #5871 (#14194)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-30 17:32:18 +08:00
fufesou
1e6bfa7bb1 fix(iPad): Magic Mouse, click (#14188)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-29 15:25:44 +08:00
9 changed files with 267 additions and 216 deletions

View File

@@ -1,79 +0,0 @@
# Log File Management
## Overview
RustDesk uses `flexi_logger` for logging with automatic log rotation. Logs are written to platform-specific directories and rotated to prevent excessive disk usage.
## Current Implementation
### Log Locations
- **macOS**: `~/Library/Logs/RustDesk/`
- **Linux**: `~/.local/share/logs/RustDesk/`
- **Android**: `{APP_HOME}/RustDesk/Logs/`
- **Windows**: Config directory → `log/` subdirectory
### Current Rotation Policy
The logging system (implemented in `libs/hbb_common/src/lib.rs`) uses size-based rotation:
```rust
.rotate(
// Rotate logs daily OR when they reach 100MB (whichever comes first)
Criterion::AgeOrSize(Age::Day, 100_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(31),
)
```
### Benefits
1. **Bounded Disk Usage**: Maximum log storage becomes ~3.1GB (31 files × 100MB)
2. **Maintained Organization**: Daily rotation still occurs for time-based organization
3. **Prevents Runaway Logs**: Heavy activity days can't create multi-gigabyte single files
4. **Automatic Cleanup**: Old files still automatically deleted after 31 days
### Implementation
The implementation is in the `hbb_common` library:
**File**: `libs/hbb_common/src/lib.rs`
**Function**: `init_log()`
**Line**: ~407
```diff
- Criterion::Age(Age::Day),
+ // Rotate logs daily OR when they reach 100MB to prevent excessive disk usage
+ // With 31 files max, this limits total log storage to ~3.1GB
+ Criterion::AgeOrSize(Age::Day, 100_000_000),
```
## Tuning Parameters
The 100MB size limit can be adjusted based on deployment needs:
- **50MB** (`50_000_000`): More conservative, max ~1.5GB total
- **100MB** (`100_000_000`): Balanced approach, max ~3.1GB total ✅ Recommended
- **200MB** (`200_000_000`): Permissive, max ~6.2GB total
## Monitoring
Users can monitor log disk usage at the locations listed above. The rotation ensures:
1. Logs older than 31 days are automatically deleted
2. Individual files never exceed the configured size limit
3. Total disk usage is bounded and predictable
## Testing
To verify the rotation works correctly:
1. Check log directory before and after rotation
2. Verify old files are cleaned up after 31 days
3. Monitor file sizes don't exceed the configured limit
4. Ensure logs still contain all necessary debugging information
## References
- flexi_logger documentation: https://docs.rs/flexi_logger/
- RustDesk logging implementation: `libs/hbb_common/src/lib.rs`

View File

@@ -1,39 +0,0 @@
# Patches for hbb_common
This directory contains patches for reference. The changes described have already been applied to the `hbb_common` submodule in this PR.
## hbb_common-log-rotation.patch
**Status**: ✅ Already applied in this PR (submodule commit 0c401fd)
**Purpose**: Add size-based log rotation to prevent excessive disk usage
**Apply to**: `libs/hbb_common` submodule
**Target repository**: https://github.com/rustdesk/hbb_common
### Reference Information
This patch file is provided for:
- Documentation of the exact changes made
- Reference for maintainers
- Potential cherry-picking to other branches if needed
The changes have already been implemented in the submodule updated by this PR.
### What it does
- Changes log rotation from age-only to age-or-size based
- Rotates logs when they reach 100MB OR daily (whichever comes first)
- Limits total log storage to ~3.1GB (31 files × 100MB max each)
- Prevents runaway log files from consuming excessive disk space
### Testing
After applying the patch and rebuilding:
1. Verify logs are created in the standard location
2. Check that individual log files don't exceed 100MB
3. Confirm old files are still cleaned up after 31 days
4. Ensure log content is still complete and useful
See `../LOG_MANAGEMENT.md` for complete documentation.

View File

@@ -1,32 +0,0 @@
From: GitHub Copilot <github-copilot@github.com>
Subject: [PATCH] Add size-based log rotation to prevent excessive disk usage
This patch adds size-based rotation to the flexi_logger configuration
to prevent individual log files from growing unbounded. Logs will now
rotate when they reach 100MB OR daily (whichever comes first).
With the existing 31-file cleanup policy, this limits total log storage
to approximately 3.1GB, preventing excessive disk space consumption.
---
src/lib.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib.rs b/src/lib.rs
index d9713b9..ba21e90 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -402,7 +402,9 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
})
.format(opt_format)
.rotate(
- Criterion::Age(Age::Day),
+ // Rotate logs daily OR when they reach 100MB to prevent excessive disk usage
+ // With 31 files max, this limits total log storage to ~3.1GB
+ Criterion::AgeOrSize(Age::Day, 100_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(31),
)
--
2.43.0

View File

@@ -501,10 +501,10 @@ class MyTheme {
)
: null,
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19),
titleSmall: TextStyle(fontSize: 14),
bodySmall: TextStyle(fontSize: 12, height: 1.25),
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
titleLarge: TextStyle(fontSize: 19, color: Colors.white70),
titleSmall: TextStyle(fontSize: 14, color: Colors.white70),
bodySmall: TextStyle(fontSize: 12, color: Colors.white70, height: 1.25),
bodyMedium: TextStyle(fontSize: 14, color: Colors.white70, height: 1.25),
labelLarge: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
@@ -1124,18 +1124,23 @@ class CustomAlertDialog extends StatelessWidget {
Widget createDialogContent(String text) {
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
bool hasLink = linkRegExp.hasMatch(text);
// Early return: no link, use default theme color
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
final List<TextSpan> spans = [];
int start = 0;
bool hasLink = false;
linkRegExp.allMatches(text).forEach((match) {
hasLink = true;
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(
text: match.group(0) ?? '',
style: TextStyle(
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
@@ -1153,13 +1158,9 @@ Widget createDialogContent(String text) {
spans.add(TextSpan(text: text.substring(start)));
}
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
return SelectableText.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 15),
style: const TextStyle(fontSize: 15),
children: spans,
),
);

View File

@@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState
// For mouse mode, we need to block the events when the cursor is in a blocked area.
// So we need to cache the last tap down position.
Offset? _lastTapDownPositionForMouseMode;
// Cache global position for onTap (which lacks position info).
Offset? _lastTapDownGlobalPosition;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
@@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState
onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
_lastTapDownGlobalPosition = d.globalPosition;
if (isNotTouchBasedDevice()) {
return;
}
@@ -154,6 +157,10 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
return;
}
if (handleTouch) {
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
@@ -171,6 +178,11 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
final lastPos = _lastTapDownGlobalPosition;
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
return;
}
if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
@@ -41,6 +42,9 @@ class _TerminalPageState extends State<TerminalPage>
final GlobalKey _keyboardKey = GlobalKey();
double _keyboardHeight = 0;
late bool _showTerminalExtraKeys;
// For iOS edge swipe gesture
double _swipeStartX = 0;
double _swipeCurrentX = 0;
// For web only.
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
@@ -147,7 +151,7 @@ class _TerminalPageState extends State<TerminalPage>
}
Widget buildBody() {
return Scaffold(
final scaffold = Scaffold(
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Stack(
@@ -192,9 +196,108 @@ class _TerminalPageState extends State<TerminalPage>
),
),
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
// iOS-style circular close button in top-right corner
if (isIOS) _buildCloseButton(),
],
),
);
// Add iOS edge swipe gesture to exit (similar to Android back button)
if (isIOS) {
return LayoutBuilder(
builder: (context, constraints) {
final screenWidth = constraints.maxWidth;
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(
debugOwner: this,
// Only respond to touch input, exclude mouse/trackpad
supportedDevices: kTouchBasedDeviceKinds,
),
(HorizontalDragGestureRecognizer instance) {
instance
// Capture initial touch-down position (before touch slop)
..onDown = (details) {
_swipeStartX = details.localPosition.dx;
_swipeCurrentX = details.localPosition.dx;
}
..onUpdate = (details) {
_swipeCurrentX = details.localPosition.dx;
}
..onEnd = (details) {
// Check if swipe started from left edge and moved right
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
clientClose(sessionId, _ffi);
}
_swipeStartX = 0;
_swipeCurrentX = 0;
}
..onCancel = () {
_swipeStartX = 0;
_swipeCurrentX = 0;
};
},
),
},
child: scaffold,
);
},
);
}
return scaffold;
}
Widget _buildCloseButton() {
return Positioned(
top: 0,
right: 0,
child: SafeArea(
minimum: const EdgeInsets.only(
top: 16, // iOS standard margin
right: 16, // iOS standard margin
),
child: Semantics(
button: true,
label: translate('Close'),
child: Container(
width: 44, // iOS standard tap target size
height: 44,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5), // Half transparency
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
clientClose(sessionId, _ffi);
},
child: Tooltip(
message: translate('Close'),
child: const Icon(
Icons.chevron_left, // iOS-style back arrow
color: Colors.white,
size: 28,
),
),
),
),
),
),
),
);
}
Widget _buildFloatingKeyboard() {

View File

@@ -59,7 +59,8 @@ class CanvasCoords {
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
model.scrollStyle =
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
@@ -418,6 +419,74 @@ class InputModel {
});
}
// https://github.com/flutter/flutter/issues/157241
// Infer CapsLock state from the character output.
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
// incorrect CapsLock state on iOS.
bool _getIosCapsFromCharacter(KeyEvent e) {
if (!isIOS) return false;
final ch = e.character;
return _getIosCapsFromCharacterImpl(
ch, HardwareKeyboard.instance.isShiftPressed);
}
// RawKeyEvent version of _getIosCapsFromCharacter.
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
if (!isIOS) return false;
final ch = e.character;
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
}
// Shared implementation for inferring CapsLock state from character.
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
//
// Limitations:
// 1. This inference assumes the client and server use the same keyboard layout.
// If layouts differ (e.g., client uses EN, server uses DE), the character output
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
// layout, making it impossible to correctly infer CapsLock state from the
// character alone.
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
// produces lowercase). This method cannot handle that case correctly.
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
if (ch == null || ch.length != 1) return false;
// Use Dart's built-in Unicode-aware case detection
final upper = ch.toUpperCase();
final lower = ch.toLowerCase();
final isUpper = upper == ch && lower != ch;
final isLower = lower == ch && upper != ch;
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
if (!isUpper && !isLower) return false;
return isUpper != shiftPressed;
}
int _buildLockModes(bool iosCapsLock) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (isIOS) {
if (iosCapsLock) {
lockModes |= (1 << capslock);
}
// Ignore "NumLock/ScrollLock" on iOS for now.
} else {
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
}
return lockModes;
}
// This function must be called after the peer info is received.
// Because `sessionGetKeyboardMode` relies on the peer version.
updateKeyboardMode() async {
@@ -550,6 +619,11 @@ class InputModel {
return KeyEventResult.handled;
}
bool iosCapsLock = false;
if (isIOS && e is RawKeyDownEvent) {
iosCapsLock = _getIosCapsFromRawCharacter(e);
}
final key = e.logicalKey;
if (e is RawKeyDownEvent) {
if (!e.repeat) {
@@ -586,7 +660,7 @@ class InputModel {
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e);
mapKeyboardModeRaw(e, iosCapsLock);
} else {
legacyKeyboardModeRaw(e);
}
@@ -622,6 +696,11 @@ class InputModel {
return KeyEventResult.handled;
}
bool iosCapsLock = false;
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
iosCapsLock = _getIosCapsFromCharacter(e);
}
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
@@ -667,7 +746,8 @@ class InputModel {
e.character ?? '',
e.physicalKey.usbHidUsage & 0xFFFF,
// Show repeat event be converted to "release+press" events?
e is KeyDownEvent || e is KeyRepeatEvent);
e is KeyDownEvent || e is KeyRepeatEvent,
iosCapsLock);
} else {
legacyKeyboardMode(e);
}
@@ -676,23 +756,9 @@ class InputModel {
}
/// Send Key Event
void newKeyboardMode(String character, int usbHid, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
void newKeyboardMode(
String character, int usbHid, bool down, bool iosCapsLock) {
final lockModes = _buildLockModes(iosCapsLock);
bind.sessionHandleFlutterKeyEvent(
sessionId: sessionId,
character: character,
@@ -701,7 +767,7 @@ class InputModel {
downOrUp: down);
}
void mapKeyboardModeRaw(RawKeyEvent e) {
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
int positionCode = -1;
int platformCode = -1;
bool down;
@@ -732,27 +798,14 @@ class InputModel {
} else {
down = false;
}
inputRawKey(e.character ?? '', platformCode, positionCode, down);
inputRawKey(
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
}
/// Send raw Key Event
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
void inputRawKey(String name, int platformCode, int positionCode, bool down,
bool iosCapsLock) {
final lockModes = _buildLockModes(iosCapsLock);
bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId,
name: name,
@@ -826,6 +879,9 @@ class InputModel {
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
bool hasStaleButtonsOnMouseUp =
type == _kMouseEventUp && evt.buttons == _lastButtons;
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
if (type == _kMouseEventMove) {
@@ -850,7 +906,7 @@ class InputModel {
buttons = evt.buttons;
}
}
_lastButtons = evt.buttons;
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
out['buttons'] = buttons;
out['type'] = type;
@@ -1218,6 +1274,28 @@ class InputModel {
_trackpadLastDelta = Offset.zero;
}
// iOS Magic Mouse duplicate event detection.
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
// for the same click in certain areas (like top-left corner).
int _lastMouseDownTimeMs = 0;
ui.Offset _lastMouseDownPos = ui.Offset.zero;
/// Check if a touch tap event should be ignored because it's a duplicate
/// of a recent mouse event (iOS Magic Mouse issue).
bool shouldIgnoreTouchTap(ui.Offset pos) {
if (!isIOS) return false;
final nowMs = DateTime.now().millisecondsSinceEpoch;
final dt = nowMs - _lastMouseDownTimeMs;
final distance = (_lastMouseDownPos - pos).distance;
// If touch tap is within 2000ms and 80px of the last mouse down,
// it's likely a duplicate event from the same Magic Mouse click.
if (dt >= 0 && dt < 2000 && distance < 80.0) {
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
return true;
}
return false;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1227,6 +1305,13 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
if (_relativeMouse.enabled.value) {
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
}
@@ -1768,9 +1853,9 @@ class InputModel {
// Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async {
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
await Future.delayed(Duration(milliseconds: 100));
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
}
Future<void> onMobileVolumeUp() async =>

View File

@@ -729,15 +729,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server-oss-not-support-tip", "注意RustDesk 開源伺服器 (OSS server) 不包含此功能。"),
("input note here", "輸入備註"),
("note-at-conn-end-tip", "在連接結束時請求備註"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Show terminal extra keys", "顯示終端機額外按鍵"),
("Relative mouse mode", "相對滑鼠模式"),
("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"),
("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"),
("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"),
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"),
("Changelog", "更新日誌"),
("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"),
("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"),
].iter().cloned().collect();
}