Compare commits

..

5 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
4 changed files with 140 additions and 182 deletions

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

@@ -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

@@ -55,10 +55,6 @@ pub struct HwRamEncoder {
pub pixfmt: AVPixelFormat,
bitrate: u32, //kbs
config: HwRamEncoderConfig,
// Frame statistics for quality monitoring
frame_count: u64,
total_frame_size: u64,
last_quality_log: std::time::Instant,
}
impl EncoderApi for HwRamEncoder {
@@ -98,35 +94,13 @@ impl EncoderApi for HwRamEncoder {
}
};
match Encoder::new(ctx.clone()) {
Ok(encoder) => {
// Log detailed encoder information for diagnostics
log::info!(
"Hardware encoder created successfully: name='{}', format={:?}, resolution={}x{}, bitrate={} kbps, fps={}, gop={}, rate_control={:?}",
config.name,
format,
config.width,
config.height,
bitrate,
DEFAULT_FPS,
gop,
rc
);
// Log GPU signature for hardware-specific issue tracking
let gpu_sig = hwcodec::common::get_gpu_signature();
if !gpu_sig.is_empty() {
log::info!("GPU signature: {}", gpu_sig);
}
Ok(HwRamEncoder {
encoder,
format,
pixfmt: ctx.pixfmt,
bitrate,
config,
frame_count: 0,
total_frame_size: 0,
last_quality_log: std::time::Instant::now(),
})
}
Ok(encoder) => Ok(HwRamEncoder {
encoder,
format,
pixfmt: ctx.pixfmt,
bitrate,
config,
}),
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
}
}
@@ -197,7 +171,6 @@ impl EncoderApi for HwRamEncoder {
}
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
let old_bitrate = self.bitrate;
let mut bitrate = Self::bitrate(
&self.config.name,
self.config.width,
@@ -208,22 +181,6 @@ impl EncoderApi for HwRamEncoder {
bitrate = Self::check_bitrate_range(&self.config, bitrate);
self.encoder.set_bitrate(bitrate as _).ok();
self.bitrate = bitrate;
// Log quality changes for hardware-specific diagnostics
if old_bitrate != bitrate {
log::info!(
"Hardware encoder quality changed: encoder='{}', ratio={:.2}, bitrate {} -> {} kbps",
self.config.name,
ratio,
old_bitrate,
bitrate
);
}
// Reset statistics on quality change
self.frame_count = 0;
self.total_frame_size = 0;
self.last_quality_log = std::time::Instant::now();
}
self.config.quality = ratio;
Ok(())
@@ -277,43 +234,6 @@ impl HwRamEncoder {
Ok(v) => {
let mut data = Vec::<EncodeFrame>::new();
data.append(v);
// Monitor encoding quality by tracking frame sizes
if !data.is_empty() {
self.frame_count += data.len() as u64;
let frame_sizes: u64 = data.iter().map(|f| f.data.len() as u64).sum();
self.total_frame_size += frame_sizes;
// Log quality statistics every 300 frames (10 seconds at 30fps)
if self.frame_count % 300 == 0 && self.last_quality_log.elapsed().as_secs() >= 10 {
let avg_frame_size = self.total_frame_size / self.frame_count;
let expected_frame_size = (self.bitrate as u64 * 1000) / (8 * DEFAULT_FPS as u64);
// Log if actual frame size is significantly different from expected
let ratio = avg_frame_size as f64 / expected_frame_size as f64;
if ratio < 0.3 || ratio > 3.0 {
log::warn!(
"Hardware encoder quality issue detected: encoder='{}', avg_frame_size={} bytes, expected={} bytes, ratio={:.2}, bitrate={} kbps",
self.config.name,
avg_frame_size,
expected_frame_size,
ratio,
self.bitrate
);
} else {
log::debug!(
"Hardware encoder stats: encoder='{}', frames={}, avg_size={} bytes, expected={} bytes, ratio={:.2}",
self.config.name,
self.frame_count,
avg_frame_size,
expected_frame_size,
ratio
);
}
self.last_quality_log = std::time::Instant::now();
}
}
Ok(data)
}
Err(_) => Ok(Vec::<EncodeFrame>::new()),
@@ -332,18 +252,6 @@ impl HwRamEncoder {
Self::calc_bitrate(width, height, ratio, name.contains("h264"))
}
/// Calculate bitrate for hardware encoders based on resolution and quality ratio.
///
/// NOTE: Hardware encoder quality can vary significantly across different GPUs/drivers.
/// Some hardware may require higher bitrates than others to achieve acceptable quality.
/// The multipliers below provide a baseline, but specific hardware (especially older
/// GPUs or certain driver versions) may still produce poor quality output even with
/// these settings. Monitor logs for "Hardware encoder quality issue detected" warnings.
///
/// If quality issues persist on specific hardware:
/// - Check GPU driver version and update if needed
/// - Consider forcing VP8/VP9 software codec as fallback
/// - File bug report with GPU model and driver version
pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 {
let base = base_bitrate(width as _, height as _) as f32 * ratio;
let threshold = 2000.0;
@@ -356,21 +264,17 @@ impl HwRamEncoder {
5.0
}
} else if h264 {
// Increased base multiplier from 2.0 to 2.5 to improve image quality
// while maintaining H264's compression efficiency
if base > threshold {
1.0 + 1.5 / (1.0 + (base - threshold) * decay_rate)
} else {
2.5
}
} else {
// H265: Increased base multiplier from 1.5 to 2.0 to fix poor image quality
// H265 should be more efficient than H264, but needs sufficient bitrate
if base > threshold {
1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate)
} else {
2.0
}
} else {
if base > threshold {
1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate)
} else {
1.5
}
};
(base * factor) as u32
}
@@ -857,53 +761,3 @@ pub fn start_check_process() {
std::thread::spawn(f);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_h264_h265_bitrate_calculation() {
// Test with 1920x1080 resolution (base_bitrate() returns 2073 kbps for 1080p)
let width = 1920;
let height = 1080;
// Test with BR_BALANCED (0.67) - default quality setting
let balanced_ratio = 0.67;
let h264_balanced = HwRamEncoder::calc_bitrate(width, height, balanced_ratio, true);
let h265_balanced = HwRamEncoder::calc_bitrate(width, height, balanced_ratio, false);
// H265 should get ~2777 kbps with new multiplier (was ~2084 with old 1.5x)
assert!(h265_balanced >= 2700 && h265_balanced <= 2850,
"H265 balanced bitrate should be ~2777 kbps, got {} kbps", h265_balanced);
// H264 should get ~3472 kbps with new multiplier (was ~2778 with old 2.0x)
assert!(h264_balanced >= 3400 && h264_balanced <= 3550,
"H264 balanced bitrate should be ~3472 kbps, got {} kbps", h264_balanced);
// H264 should have higher bitrate than H265 at same quality
assert!(h264_balanced > h265_balanced,
"H264 should have higher bitrate than H265");
// Test with BR_BEST (1.5) - best quality setting
let best_ratio = 1.5;
let h265_best = HwRamEncoder::calc_bitrate(width, height, best_ratio, false);
// At best quality, should use significantly more bitrate (>50% more)
assert!((h265_best as f64) > (h265_balanced as f64 * 1.5),
"Best quality should use >50% more bitrate than balanced");
// Test with BR_SPEED (0.5) - low quality setting
let speed_ratio = 0.5;
let h265_speed = HwRamEncoder::calc_bitrate(width, height, speed_ratio, false);
// At speed quality, should use less bitrate
assert!(h265_speed < h265_balanced,
"Speed quality should use less bitrate than balanced");
// Verify bitrate scales proportionally with resolution
let hd_bitrate = HwRamEncoder::calc_bitrate(1280, 720, balanced_ratio, false);
assert!(hd_bitrate < h265_balanced,
"720p should use less bitrate than 1080p");
}
}

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();
}