Merge branch 'rustdesk:master' into chat

This commit is contained in:
NicKoehler 2023-05-20 18:35:03 +02:00 committed by GitHub
commit 717a7e9e03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1087 additions and 295 deletions

4
Cargo.lock generated
View File

@ -3558,9 +3558,10 @@ dependencies = [
[[package]] [[package]]
name = "magnum-opus" name = "magnum-opus"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/rustdesk/magnum-opus#79be072c939168e907fe851690759dcfd6a326af" source = "git+https://github.com/rustdesk/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"pkg-config",
"target_build_utils", "target_build_utils",
] ]
@ -5431,6 +5432,7 @@ dependencies = [
"log", "log",
"ndk 0.7.0", "ndk 0.7.0",
"num_cpus", "num_cpus",
"pkg-config",
"quest", "quest",
"repng", "repng",
"serde 1.0.163", "serde 1.0.163",

View File

@ -32,6 +32,7 @@ mediacodec = ["scrap/mediacodec"]
linux_headless = ["pam" ] linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"] virtual_display_driver = ["virtual_display"]
plugin_framework = [] plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -19,8 +19,6 @@ import '../../common/widgets/peer_tab_page.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/button.dart'; import '../widgets/button.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget { class ConnectionPage extends StatefulWidget {
const ConnectionPage({Key? key}) : super(key: key); const ConnectionPage({Key? key}) : super(key: key);

View File

@ -660,69 +660,24 @@ class _ControlMenu extends StatelessWidget {
} }
} }
class _DisplayMenu extends StatefulWidget { class ScreenAdjustor {
final String id; final String id;
final FFI ffi; final FFI ffi;
final MenubarState state; final VoidCallback cbExitFullscreen;
final Function(bool) setFullscreen;
final Widget pluginItem;
_DisplayMenu(
{Key? key,
required this.id,
required this.ffi,
required this.state,
required this.setFullscreen})
: pluginItem = LocationItem.createLocationItem(
id,
ffi,
kLocationClientRemoteToolbarDisplay,
true,
),
super(key: key);
@override
State<_DisplayMenu> createState() => _DisplayMenuState();
}
class _DisplayMenuState extends State<_DisplayMenu> {
window_size.Screen? _screen; window_size.Screen? _screen;
ScreenAdjustor({
required this.id,
required this.ffi,
required this.cbExitFullscreen,
});
bool get isFullscreen => stateGlobal.fullscreen; bool get isFullscreen => stateGlobal.fullscreen;
int get windowId => stateGlobal.windowId; int get windowId => stateGlobal.windowId;
Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
FFI get ffi => widget.ffi;
String get id => widget.id;
@override
Widget build(BuildContext context) {
_updateScreen();
return _IconSubmenuButton(
tooltip: 'Display Settings',
svg: "assets/display.svg",
ffi: widget.ffi,
color: _MenubarTheme.blueColor,
hoverColor: _MenubarTheme.hoverBlueColor,
menuChildren: [
adjustWindow(),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
resolutions(),
Divider(),
toggles(),
widget.pluginItem,
]);
}
adjustWindow() { adjustWindow() {
return futureBuilder( return futureBuilder(
future: _isWindowCanBeAdjusted(), future: isWindowCanBeAdjusted(),
hasData: (data) { hasData: (data) {
final visible = data as bool; final visible = data as bool;
if (!visible) return Offstage(); if (!visible) return Offstage();
@ -730,18 +685,18 @@ class _DisplayMenuState extends State<_DisplayMenu> {
children: [ children: [
MenuButton( MenuButton(
child: Text(translate('Adjust Window')), child: Text(translate('Adjust Window')),
onPressed: _doAdjustWindow, onPressed: doAdjustWindow,
ffi: widget.ffi), ffi: ffi),
Divider(), Divider(),
], ],
); );
}); });
} }
_doAdjustWindow() async { doAdjustWindow() async {
await _updateScreen(); await updateScreen();
if (_screen != null) { if (_screen != null) {
widget.setFullscreen(false); cbExitFullscreen();
double scale = _screen!.scaleFactor; double scale = _screen!.scaleFactor;
final wndRect = await WindowController.fromWindowId(windowId).getFrame(); final wndRect = await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromWindow(ui.window).size; final mediaSize = MediaQueryData.fromWindow(ui.window).size;
@ -752,7 +707,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
double magicHeight = double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale; wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = widget.ffi.canvasModel; final canvasModel = ffi.canvasModel;
final width = (canvasModel.getDisplayWidth() * canvasModel.scale + final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
CanvasModel.leftToEdge + CanvasModel.leftToEdge +
CanvasModel.rightToEdge) * CanvasModel.rightToEdge) *
@ -787,7 +742,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
} }
} }
_updateScreen() async { updateScreen() async {
final v = await rustDeskWinManager.call( final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, ''); WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v; final String valueStr = v;
@ -807,8 +762,8 @@ class _DisplayMenuState extends State<_DisplayMenu> {
} }
} }
Future<bool> _isWindowCanBeAdjusted() async { Future<bool> isWindowCanBeAdjusted() async {
final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; final viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) { if (viewStyle != kRemoteViewStyleOriginal) {
return false; return false;
} }
@ -827,7 +782,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
selfHeight = _screen!.frame.height; selfHeight = _screen!.frame.height;
} }
final canvasModel = widget.ffi.canvasModel; final canvasModel = ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth(); final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight(); final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth = final requiredWidth =
@ -837,6 +792,77 @@ class _DisplayMenuState extends State<_DisplayMenu> {
return selfWidth > (requiredWidth * scale) && return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale); selfHeight > (requiredHeight * scale);
} }
}
class _DisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
final MenubarState state;
final Function(bool) setFullscreen;
final Widget pluginItem;
_DisplayMenu(
{Key? key,
required this.id,
required this.ffi,
required this.state,
required this.setFullscreen})
: pluginItem = LocationItem.createLocationItem(
id,
ffi,
kLocationClientRemoteToolbarDisplay,
true,
),
super(key: key);
@override
State<_DisplayMenu> createState() => _DisplayMenuState();
}
class _DisplayMenuState extends State<_DisplayMenu> {
late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
id: widget.id,
ffi: widget.ffi,
cbExitFullscreen: () => widget.setFullscreen(false),
);
bool get isFullscreen => stateGlobal.fullscreen;
int get windowId => stateGlobal.windowId;
Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
FFI get ffi => widget.ffi;
String get id => widget.id;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
return _IconSubmenuButton(
tooltip: 'Display Settings',
svg: "assets/display.svg",
ffi: widget.ffi,
color: _MenubarTheme.blueColor,
hoverColor: _MenubarTheme.hoverBlueColor,
menuChildren: [
_screenAdjustor.adjustWindow(),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
Divider(),
toggles(),
widget.pluginItem,
]);
}
viewStyle() { viewStyle() {
return futureBuilder( return futureBuilder(
@ -935,46 +961,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
}); });
} }
resolutions() {
final resolutions = pi.resolutions;
final visible = ffiModel.keyboard && resolutions.length > 1;
if (!visible) return Offstage();
final display = ffiModel.display;
final groupValue = "${display.width}x${display.height}";
onChanged(String? value) async {
if (value == null) return;
final list = value.split('x');
if (list.length == 2) {
final w = int.tryParse(list[0]);
final h = int.tryParse(list[1]);
if (w != null && h != null) {
await bind.sessionChangeResolution(
id: widget.id, width: w, height: h);
Future.delayed(Duration(seconds: 3), () async {
final display = ffiModel.display;
if (w == display.width && h == display.height) {
if (await _isWindowCanBeAdjusted()) {
_doAdjustWindow();
}
}
});
}
}
}
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: resolutions
.map((e) => RdoMenuButton(
value: '${e.width}x${e.height}',
groupValue: groupValue,
onChanged: onChanged,
ffi: widget.ffi,
child: Text('${e.width}x${e.height}')))
.toList(),
child: Text(translate("Resolution")));
}
toggles() { toggles() {
return futureBuilder( return futureBuilder(
future: toolbarDisplayToggle(context, id, ffi), future: toolbarDisplayToggle(context, id, ffi),
@ -993,6 +979,263 @@ class _DisplayMenuState extends State<_DisplayMenu> {
} }
} }
class _ResolutionsMenu extends StatefulWidget {
final String id;
final FFI ffi;
final ScreenAdjustor screenAdjustor;
_ResolutionsMenu({
Key? key,
required this.id,
required this.ffi,
required this.screenAdjustor,
}) : super(key: key);
@override
State<_ResolutionsMenu> createState() => _ResolutionsMenuState();
}
const double _kCustonResolutionEditingWidth = 42;
const _kCustomResolutionValue = 'custom';
class _ResolutionsMenuState extends State<_ResolutionsMenu> {
String _groupValue = '';
Resolution? _localResolution;
late final TextEditingController _customWidth =
TextEditingController(text: display.width.toString());
late final TextEditingController _customHeight =
TextEditingController(text: display.height.toString());
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display;
List<Resolution> get resolutions => pi.resolutions;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final isVirtualDisplay = display.isVirtualDisplayResolution;
final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage();
_getLocalResolution();
final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
_setGroupValue();
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: <Widget>[
_OriginalResolutionMenuButton(showOriginalBtn),
_FitLocalResolutionMenuButton(showFitLocalBtn),
_customResolutionMenuButton(isVirtualDisplay),
_menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay),
] +
_supportedResolutionMenuButtons(),
child: Text(translate("Resolution")),
);
}
_setGroupValue() {
final lastGroupValue =
stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
if (lastGroupValue == _kCustomResolutionValue) {
_groupValue = _kCustomResolutionValue;
} else {
_groupValue = '${display.width}x${display.height}';
}
}
_menuDivider(
bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) {
return Offstage(
offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay),
child: Divider(),
);
}
_getLocalResolution() {
_localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay();
if (currentDisplay.isNotEmpty) {
try {
final display = json.decode(currentDisplay);
if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']);
}
} catch (e) {
debugPrint('Failed to decode $currentDisplay, $e');
}
}
}
_onChanged(String? value) async {
stateGlobal.setLastResolutionGroupValue(
widget.id, pi.currentDisplay, value);
if (value == null) return;
int? w;
int? h;
if (value == _kCustomResolutionValue) {
w = int.tryParse(_customWidth.text);
h = int.tryParse(_customHeight.text);
} else {
final list = value.split('x');
if (list.length == 2) {
w = int.tryParse(list[0]);
h = int.tryParse(list[1]);
}
}
if (w != null && h != null) {
if (w != display.width || h != display.height) {
await _changeResolution(w, h);
}
}
}
_changeResolution(int w, int h) async {
await bind.sessionChangeResolution(
id: widget.id,
width: w,
height: h,
);
Future.delayed(Duration(seconds: 3), () async {
final display = ffiModel.display;
if (w == display.width && h == display.height) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow();
}
}
});
}
Widget _OriginalResolutionMenuButton(bool showOriginalBtn) {
return Offstage(
offstage: !showOriginalBtn,
child: MenuButton(
onPressed: () =>
_changeResolution(display.originalWidth, display.originalHeight),
ffi: widget.ffi,
child: Text(
'${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'),
),
);
}
Widget _FitLocalResolutionMenuButton(bool showFitLocalBtn) {
return Offstage(
offstage: !showFitLocalBtn,
child: MenuButton(
onPressed: () {
final resolution = _getBestFitResolution();
if (resolution != null) {
_changeResolution(resolution.width, resolution.height);
}
},
ffi: widget.ffi,
child: Text(
'${translate('resolution_fit_local_tip')} ${_localResolution?.width ?? 0}x${_localResolution?.height ?? 0}'),
),
);
}
Widget _customResolutionMenuButton(isVirtualDisplay) {
return Offstage(
offstage: !isVirtualDisplay,
child: RdoMenuButton(
value: _kCustomResolutionValue,
groupValue: _groupValue,
onChanged: _onChanged,
ffi: widget.ffi,
child: Row(
children: [
Text('${translate('resolution_custom_tip')} '),
SizedBox(
width: _kCustonResolutionEditingWidth,
child: _resolutionInput(_customWidth),
),
Text(' x '),
SizedBox(
width: _kCustonResolutionEditingWidth,
child: _resolutionInput(_customHeight),
),
],
),
),
);
}
TextField _resolutionInput(TextEditingController controller) {
return TextField(
decoration: InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.fromLTRB(3, 3, 3, 3),
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
controller: controller,
);
}
List<Widget> _supportedResolutionMenuButtons() => resolutions
.map((e) => RdoMenuButton(
value: '${e.width}x${e.height}',
groupValue: _groupValue,
onChanged: _onChanged,
ffi: widget.ffi,
child: Text('${e.width}x${e.height}')))
.toList();
Resolution? _getBestFitResolution() {
if (_localResolution == null) {
return null;
}
if (display.isVirtualDisplayResolution) {
return _localResolution!;
}
squareDistance(Resolution lhs, Resolution rhs) =>
(lhs.width - rhs.width) * (lhs.width - rhs.width) +
(lhs.height - rhs.height) * (lhs.height - rhs.height);
Resolution res = Resolution(display.width, display.height);
for (final r in resolutions) {
if (r.width <= _localResolution!.width &&
r.height <= _localResolution!.height) {
if (squareDistance(r, _localResolution!) <
squareDistance(res, _localResolution!)) {
res = r;
}
}
}
return res;
}
bool _isRemoteResolutionFitLocal() {
if (_localResolution == null) {
return true;
}
final bestFitResolution = _getBestFitResolution();
if (bestFitResolution == null) {
return true;
}
return bestFitResolution.width == display.width &&
bestFitResolution.height == display.height;
}
}
class _KeyboardMenu extends StatelessWidget { class _KeyboardMenu extends StatelessWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
@ -1483,14 +1726,14 @@ class RdoMenuButton<T> extends StatelessWidget {
final ValueChanged<T?>? onChanged; final ValueChanged<T?>? onChanged;
final Widget? child; final Widget? child;
final FFI ffi; final FFI ffi;
const RdoMenuButton( const RdoMenuButton({
{Key? key, Key? key,
required this.value, required this.value,
required this.groupValue, required this.groupValue,
required this.onChanged, required this.child,
required this.child, required this.ffi,
required this.ffi}) this.onChanged,
: super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -295,11 +295,15 @@ class FfiModel with ChangeNotifier {
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) { handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
_pi.currentDisplay = int.parse(evt['display']); _pi.currentDisplay = int.parse(evt['display']);
var newDisplay = Display(); var newDisplay = Display();
newDisplay.x = double.parse(evt['x']); newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x;
newDisplay.y = double.parse(evt['y']); newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y;
newDisplay.width = int.parse(evt['width']); newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
newDisplay.height = int.parse(evt['height']); newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
newDisplay.originalWidth =
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
newDisplay.originalHeight =
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
_updateCurDisplay(peerId, newDisplay); _updateCurDisplay(peerId, newDisplay);
@ -466,14 +470,7 @@ class FfiModel with ChangeNotifier {
_pi.displays = []; _pi.displays = [];
List<dynamic> displays = json.decode(evt['displays']); List<dynamic> displays = json.decode(evt['displays']);
for (int i = 0; i < displays.length; ++i) { for (int i = 0; i < displays.length; ++i) {
Map<String, dynamic> d0 = displays[i]; _pi.displays.add(evtToDisplay(displays[i]));
var d = Display();
d.x = d0['x'].toDouble();
d.y = d0['y'].toDouble();
d.width = d0['width'];
d.height = d0['height'];
d.cursorEmbedded = d0['cursor_embedded'] == 1;
_pi.displays.add(d);
} }
stateGlobal.displaysCount.value = _pi.displays.length; stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay < _pi.displays.length) { if (_pi.currentDisplay < _pi.displays.length) {
@ -506,6 +503,9 @@ class FfiModel with ChangeNotifier {
} }
} }
} }
stateGlobal.resetLastResolutionGroupValues(peerId);
notifyListeners(); notifyListeners();
} }
@ -533,20 +533,25 @@ class FfiModel with ChangeNotifier {
} }
} }
Display evtToDisplay(Map<String, dynamic> evt) {
var d = Display();
d.x = evt['x']?.toDouble() ?? d.x;
d.y = evt['y']?.toDouble() ?? d.y;
d.width = evt['width'] ?? d.width;
d.height = evt['height'] ?? d.height;
d.cursorEmbedded = evt['cursor_embedded'] == 1;
d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue;
d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue;
return d;
}
/// Handle the peer info synchronization event based on [evt]. /// Handle the peer info synchronization event based on [evt].
handleSyncPeerInfo(Map<String, dynamic> evt, String peerId) async { handleSyncPeerInfo(Map<String, dynamic> evt, String peerId) async {
if (evt['displays'] != null) { if (evt['displays'] != null) {
List<dynamic> displays = json.decode(evt['displays']); List<dynamic> displays = json.decode(evt['displays']);
List<Display> newDisplays = []; List<Display> newDisplays = [];
for (int i = 0; i < displays.length; ++i) { for (int i = 0; i < displays.length; ++i) {
Map<String, dynamic> d0 = displays[i]; newDisplays.add(evtToDisplay(displays[i]));
var d = Display();
d.x = d0['x'].toDouble();
d.y = d0['y'].toDouble();
d.width = d0['width'];
d.height = d0['height'];
d.cursorEmbedded = d0['cursor_embedded'] == 1;
newDisplays.add(d);
} }
_pi.displays = newDisplays; _pi.displays = newDisplays;
stateGlobal.displaysCount.value = _pi.displays.length; stateGlobal.displaysCount.value = _pi.displays.length;
@ -1712,12 +1717,17 @@ class FFI {
} }
} }
const kInvalidResolutionValue = -1;
const kVirtualDisplayResolutionValue = 0;
class Display { class Display {
double x = 0; double x = 0;
double y = 0; double y = 0;
int width = 0; int width = 0;
int height = 0; int height = 0;
bool cursorEmbedded = false; bool cursorEmbedded = false;
int originalWidth = kInvalidResolutionValue;
int originalHeight = kInvalidResolutionValue;
Display() { Display() {
width = (isDesktop || isWebDesktop) width = (isDesktop || isWebDesktop)
@ -1740,6 +1750,15 @@ class Display {
other.width == width && other.width == width &&
other.height == height && other.height == height &&
other.cursorEmbedded == cursorEmbedded; other.cursorEmbedded == cursorEmbedded;
bool get isOriginalResolutionSet =>
originalWidth != kInvalidResolutionValue &&
originalHeight != kInvalidResolutionValue;
bool get isVirtualDisplayResolution =>
originalWidth == kVirtualDisplayResolutionValue &&
originalHeight == kVirtualDisplayResolutionValue;
bool get isOriginalResolution =>
width == originalWidth && height == originalHeight;
} }
class Resolution { class Resolution {

View File

@ -12,12 +12,14 @@ class StateGlobal {
bool _maximize = false; bool _maximize = false;
bool grabKeyboard = false; bool grabKeyboard = false;
final RxBool _showTabBar = true.obs; final RxBool _showTabBar = true.obs;
final RxBool _showResizeEdge = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteMenuBar = false.obs; final RxBool showRemoteMenuBar = false.obs;
final RxInt displaysCount = 0.obs; final RxInt displaysCount = 0.obs;
// Use for desktop -> remote toolbar -> resolution
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
int get windowId => _windowId; int get windowId => _windowId;
bool get fullscreen => _fullscreen; bool get fullscreen => _fullscreen;
bool get maximize => _maximize; bool get maximize => _maximize;
@ -26,6 +28,22 @@ class StateGlobal {
RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get resizeEdgeSize => _resizeEdgeSize;
RxDouble get windowBorderWidth => _windowBorderWidth; RxDouble get windowBorderWidth => _windowBorderWidth;
resetLastResolutionGroupValues(String peerId) {
_lastResolutionGroupValues[peerId] = {};
}
setLastResolutionGroupValue(
String peerId, int currentDisplay, String? value) {
if (!_lastResolutionGroupValues.containsKey(peerId)) {
_lastResolutionGroupValues[peerId] = {};
}
_lastResolutionGroupValues[peerId]![currentDisplay] = value;
}
String? getLastResolutionGroupValue(String peerId, int currentDisplay) {
return _lastResolutionGroupValues[peerId]?[currentDisplay];
}
setWindowId(int id) => _windowId = id; setWindowId(int id) => _windowId = id;
setMaximize(bool v) { setMaximize(bool v) {
if (_maximize != v && !_fullscreen) { if (_maximize != v && !_fullscreen) {
@ -33,12 +51,12 @@ class StateGlobal {
_resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
} }
} }
setFullscreen(bool v) { setFullscreen(bool v) {
if (_fullscreen != v) { if (_fullscreen != v) {
_fullscreen = v; _fullscreen = v;
_showTabBar.value = !_fullscreen; _showTabBar.value = !_fullscreen;
_resizeEdgeSize.value = _resizeEdgeSize.value = fullscreen
fullscreen
? kFullScreenEdgeSize ? kFullScreenEdgeSize
: _maximize : _maximize
? kMaximizeEdgeSize ? kMaximizeEdgeSize

View File

@ -41,6 +41,7 @@ message DisplayInfo {
string name = 5; string name = 5;
bool online = 6; bool online = 6;
bool cursor_embedded = 7; bool cursor_embedded = 7;
Resolution original_resolution = 8;
} }
message PortForward { message PortForward {
@ -444,6 +445,8 @@ message SwitchDisplay {
int32 height = 5; int32 height = 5;
bool cursor_embedded = 6; bool cursor_embedded = 6;
SupportedResolutions resolutions = 7; SupportedResolutions resolutions = 7;
// Do not care about the origin point for now.
Resolution original_resolution = 8;
} }
message PermissionInfo { message PermissionInfo {
@ -501,6 +504,7 @@ message OptionMessage {
SupportedDecoding supported_decoding = 10; SupportedDecoding supported_decoding = 10;
int32 custom_fps = 11; int32 custom_fps = 11;
BoolOption disable_keyboard = 12; BoolOption disable_keyboard = 12;
Resolution custom_resolution = 13;
} }
message TestDelay { message TestDelay {

View File

@ -105,12 +105,9 @@ macro_rules! serde_field_string {
where where
D: de::Deserializer<'de>, D: de::Deserializer<'de>,
{ {
let s: &str = de::Deserialize::deserialize(deserializer).unwrap_or_default(); let s: String =
Ok(if s.is_empty() { de::Deserialize::deserialize(deserializer).unwrap_or(Self::$default_func());
Self::$default_func() Ok(s)
} else {
s.to_owned()
})
} }
}; };
} }
@ -191,6 +188,12 @@ pub struct Config2 {
pub options: HashMap<String, String>, pub options: HashMap<String, String>,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct Resolution {
pub w: i32,
pub h: i32,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig { pub struct PeerConfig {
#[serde(default, deserialize_with = "deserialize_vec_u8")] #[serde(default, deserialize_with = "deserialize_vec_u8")]
@ -246,6 +249,13 @@ pub struct PeerConfig {
#[serde(flatten)] #[serde(flatten)]
pub view_only: ViewOnly, pub view_only: ViewOnly,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_option_resolution"
)]
pub custom_resolution: Option<Resolution>,
// The other scalar value must before this // The other scalar value must before this
#[serde(default, deserialize_with = "PeerConfig::deserialize_options")] #[serde(default, deserialize_with = "PeerConfig::deserialize_options")]
pub options: HashMap<String, String>, // not use delete to represent default values pub options: HashMap<String, String>, // not use delete to represent default values
@ -1446,7 +1456,7 @@ impl ConfigOidc {
fn _load_env(mut self) -> Self { fn _load_env(mut self) -> Self {
use std::env; use std::env;
for (k, mut v) in &mut self.providers { for (k, v) in &mut self.providers {
if let Ok(client_id) = env::var(format!("OIDC-{}-CLIENT-ID", k.to_uppercase())) { if let Ok(client_id) = env::var(format!("OIDC-{}-CLIENT-ID", k.to_uppercase())) {
v.client_id = client_id; v.client_id = client_id;
} }
@ -1486,6 +1496,7 @@ deserialize_default!(deserialize_option_string, Option<String>);
deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>); deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>);
deserialize_default!(deserialize_hashmap_string_bool, HashMap<String, bool>); deserialize_default!(deserialize_hashmap_string_bool, HashMap<String, bool>);
deserialize_default!(deserialize_hashmap_string_configoidcprovider, HashMap<String, ConfigOidcProvider>); deserialize_default!(deserialize_hashmap_string_configoidcprovider, HashMap<String, ConfigOidcProvider>);
deserialize_default!(deserialize_option_resolution, Option<Resolution>);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -1534,4 +1545,34 @@ mod tests {
}) })
); );
} }
#[test]
fn test_peer_config_deserialize() {
let default_peer_config = toml::from_str::<PeerConfig>("").unwrap();
// test custom_resolution
{
let wrong_type_str = r#"
view_style = "adaptive"
scroll_style = "scrollbar"
custom_resolution = true
"#;
let mut cfg_to_compare = default_peer_config.clone();
cfg_to_compare.view_style = "adaptive".to_string();
cfg_to_compare.scroll_style = "scrollbar".to_string();
let cfg = toml::from_str::<PeerConfig>(wrong_type_str);
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str");
let wrong_field_str = r#"
[custom_resolution]
w = 1920
h = 1080
hello = "world"
[ui_flutter]
"#;
let mut cfg_to_compare = default_peer_config.clone();
cfg_to_compare.custom_resolution = Some(Resolution { w: 1920, h: 1080 });
let cfg = toml::from_str::<PeerConfig>(wrong_field_str);
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str");
}
}
} }

View File

@ -12,6 +12,7 @@ edition = "2018"
[features] [features]
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"]
mediacodec = ["ndk"] mediacodec = ["ndk"]
linux-pkg-config = ["dep:pkg-config"]
[dependencies] [dependencies]
block = "0.1" block = "0.1"
@ -43,6 +44,7 @@ quest = "0.3"
[build-dependencies] [build-dependencies]
target_build_utils = "0.3" target_build_utils = "0.3"
bindgen = "0.65" bindgen = "0.65"
pkg-config = { version = "0.3.27", optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
dbus = { version = "0.9", optional = true } dbus = { version = "0.9", optional = true }

View File

@ -1,8 +1,28 @@
use std::{ use std::{
env, fs, env, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
println,
}; };
#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))]
fn link_pkg_config(name: &str) -> Vec<PathBuf> {
// sometimes an override is needed
let pc_name = match name {
"libvpx" => "vpx",
_ => name,
};
let lib = pkg_config::probe_library(pc_name)
.expect(format!(
"unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled).
try installing '{pc_name}-dev' from your system package manager.").as_str());
lib.include_paths
}
#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))]
fn link_pkg_config(_name: &str) -> Vec<PathBuf> {
unimplemented!()
}
/// Link vcppkg package. /// Link vcppkg package.
fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
@ -102,8 +122,16 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
} }
/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1). /// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1).
/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1)
fn find_package(name: &str) -> Vec<PathBuf> { fn find_package(name: &str) -> Vec<PathBuf> {
if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}");
println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}");
if cfg!(all(target_os = "linux", feature = "linux-pkg-config"))
&& std::env::var(no_pkg_config_var_name).as_deref() != Ok("1") {
link_pkg_config(name)
} else if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") {
vec![link_vcpkg(vcpkg_root.into(), name)] vec![link_vcpkg(vcpkg_root.into(), name)]
} else { } else {
// Try using homebrew // Try using homebrew

View File

@ -23,7 +23,7 @@ use hwcodec::{
const CFG_KEY_ENCODER: &str = "bestHwEncoders"; const CFG_KEY_ENCODER: &str = "bestHwEncoders";
const CFG_KEY_DECODER: &str = "bestHwDecoders"; const CFG_KEY_DECODER: &str = "bestHwDecoders";
const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
const DEFAULT_GOP: i32 = i32::MAX; const DEFAULT_GOP: i32 = i32::MAX;
const DEFAULT_HW_QUALITY: Quality = Quality_Default; const DEFAULT_HW_QUALITY: Quality = Quality_Default;
@ -332,6 +332,8 @@ pub fn check_config_process() {
use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; use hbb_common::sysinfo::{ProcessExt, System, SystemExt};
std::thread::spawn(move || { std::thread::spawn(move || {
// Remove to avoid checking process errors
// But when the program is just started, the configuration file has not been updated, and the new connection will read an empty configuration
HwCodecConfig::remove(); HwCodecConfig::remove();
if let Ok(exe) = std::env::current_exe() { if let Ok(exe) = std::env::current_exe() {
if let Some(file_name) = exe.file_name().to_owned() { if let Some(file_name) = exe.file_name().to_owned() {
@ -344,9 +346,16 @@ pub fn check_config_process() {
} }
} }
if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() {
let second = 3; // wait up to 10 seconds
std::thread::sleep(std::time::Duration::from_secs(second)); for _ in 0..10 {
// kill: Different platforms have different results std::thread::sleep(std::time::Duration::from_secs(1));
if let Ok(Some(status)) = child.try_wait() {
if status.success() {
HwCodecConfig::refresh();
}
break;
}
}
allow_err!(child.kill()); allow_err!(child.kill());
std::thread::sleep(std::time::Duration::from_millis(30)); std::thread::sleep(std::time::Duration::from_millis(30));
match child.try_wait() { match child.try_wait() {

View File

@ -36,7 +36,7 @@ def main():
def expand(): def expand():
for fn in glob.glob('./src/lang/*'): for fn in glob.glob('./src/lang/*.rs'):
lang = os.path.basename(fn)[:-3] lang = os.path.basename(fn)[:-3]
if lang in ['en','template']: continue if lang in ['en','template']: continue
print(lang) print(lang)

View File

@ -31,9 +31,11 @@ use hbb_common::{
allow_err, allow_err,
anyhow::{anyhow, Context}, anyhow::{anyhow, Context},
bail, bail,
config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT}, config::{
Config, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT,
},
get_version_number, log, get_version_number, log,
message_proto::{option_message::BoolOption, *}, message_proto::{option_message::BoolOption, Resolution as ProtoResolution, *},
protobuf::Message as _, protobuf::Message as _,
rand, rand,
rendezvous_proto::*, rendezvous_proto::*,
@ -1351,7 +1353,7 @@ impl LoginConfigHandler {
/// ///
/// * `ignore_default` - If `true`, ignore the default value of the option. /// * `ignore_default` - If `true`, ignore the default value of the option.
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> { fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP)
{ {
return None; return None;
} }
@ -1400,6 +1402,16 @@ impl LoginConfigHandler {
msg.disable_clipboard = BoolOption::Yes.into(); msg.disable_clipboard = BoolOption::Yes.into();
n += 1; n += 1;
} }
if let Some(r) = self.get_custom_resolution() {
if r.0 > 0 && r.1 > 0 {
msg.custom_resolution = Some(ProtoResolution {
width: r.0,
height: r.1,
..Default::default()
})
.into();
}
}
msg.supported_decoding = msg.supported_decoding =
hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(Some(&self.id))); hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(Some(&self.id)));
n += 1; n += 1;
@ -1412,7 +1424,7 @@ impl LoginConfigHandler {
} }
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> { pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP)
{ {
return None; return None;
} }
@ -1571,6 +1583,18 @@ impl LoginConfigHandler {
} }
} }
#[inline]
pub fn get_custom_resolution(&self) -> Option<(i32, i32)> {
self.config.custom_resolution.as_ref().map(|r| (r.w, r.h))
}
#[inline]
pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) {
let mut config = self.load_config();
config.custom_resolution = wh.map(|r| Resolution { w: r.0, h: r.1 });
self.save_config(config);
}
/// Get user name. /// Get user name.
/// Return the name of the given peer. If the peer has no name, return the name in the config. /// Return the name of the given peer. If the peer has no name, return the name in the config.
/// ///
@ -1689,7 +1713,7 @@ impl LoginConfigHandler {
show_hidden: !self.get_option("remote_show_hidden").is_empty(), show_hidden: !self.get_option("remote_show_hidden").is_empty(),
..Default::default() ..Default::default()
}), }),
ConnType::PORT_FORWARD => lr.set_port_forward(PortForward { ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward {
host: self.port_forward.0.clone(), host: self.port_forward.0.clone(),
port: self.port_forward.1, port: self.port_forward.1,
..Default::default() ..Default::default()

View File

@ -18,6 +18,7 @@ use hbb_common::protobuf::Message as _;
use hbb_common::rendezvous_proto::ConnType; use hbb_common::rendezvous_proto::ConnType;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::sleep; use hbb_common::sleep;
#[cfg(not(target_os = "ios"))]
use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::tokio::sync::mpsc::error::TryRecvError;
#[cfg(windows)] #[cfg(windows)]
use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::sync::Mutex as TokioMutex;
@ -1211,6 +1212,14 @@ impl<T: InvokeUiSession> Remote<T> {
s.cursor_embedded, s.cursor_embedded,
); );
} }
let custom_resolution = if s.width != s.original_resolution.width
|| s.height != s.original_resolution.height
{
Some((s.width, s.height))
} else {
None
};
self.handler.set_custom_resolution(custom_resolution);
} }
Some(misc::Union::CloseReason(c)) => { Some(misc::Union::CloseReason(c)) => {
self.handler.msgbox("error", "Connection Error", &c, ""); self.handler.msgbox("error", "Connection Error", &c, "");

View File

@ -1,10 +1,10 @@
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::platform::breakdown_callback; use crate::platform::breakdown_callback;
use hbb_common::log;
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::platform::register_breakdown_handler; use hbb_common::platform::register_breakdown_handler;
use hbb_common::{allow_err, log};
/// shared by flutter and sciter main function /// shared by flutter and sciter main function
/// ///
@ -270,7 +270,7 @@ fn init_plugins(args: &Vec<String>) {
crate::plugin::init(); crate::plugin::init();
} }
} else if "--service" == (&args[0] as &str) { } else if "--service" == (&args[0] as &str) {
allow_err!(crate::plugin::remove_uninstalled()); hbb_common::allow_err!(crate::plugin::remove_uninstalled());
} }
} }

View File

@ -286,6 +286,8 @@ impl FlutterHandler {
h.insert("width", d.width); h.insert("width", d.width);
h.insert("height", d.height); h.insert("height", d.height);
h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 });
h.insert("original_width", d.original_resolution.width);
h.insert("original_height", d.original_resolution.height);
msg_vec.push(h); msg_vec.push(h);
} }
serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned())
@ -618,6 +620,14 @@ impl InvokeUiSession for FlutterHandler {
.to_string(), .to_string(),
), ),
("resolutions", &resolutions), ("resolutions", &resolutions),
(
"original_width",
&display.original_resolution.width.to_string(),
),
(
"original_height",
&display.original_resolution.height.to_string(),
),
], ],
); );
} }

View File

@ -880,6 +880,21 @@ pub fn main_handle_relay_id(id: String) -> String {
handle_relay_id(id) handle_relay_id(id)
} }
pub fn main_get_current_display() -> SyncReturn<String> {
#[cfg(not(target_os = "ios"))]
let display_info = match crate::video_service::get_current_display() {
Ok((_, _, display)) => serde_json::to_string(&HashMap::from([
("w", display.width()),
("h", display.height()),
]))
.unwrap_or_default(),
Err(..) => "".to_string(),
};
#[cfg(target_os = "ios")]
let display_info = "".to_owned();
SyncReturn(display_info)
}
pub fn session_add_port_forward( pub fn session_add_port_forward(
id: String, id: String,
local_port: i32, local_port: i32,
@ -1426,10 +1441,10 @@ pub fn plugin_event(_id: String, _peer: String, _event: Vec<u8>) {
} }
} }
pub fn plugin_register_event_stream(id: String, event2ui: StreamSink<EventToUI>) { pub fn plugin_register_event_stream(_id: String, _event2ui: StreamSink<EventToUI>) {
#[cfg(feature = "plugin_framework")] #[cfg(feature = "plugin_framework")]
{ {
crate::plugin::native_handlers::session::session_register_event_stream(id, event2ui); crate::plugin::native_handlers::session::session_register_event_stream(_id, _event2ui);
} }
} }
@ -1577,16 +1592,16 @@ pub fn plugin_list_reload() {
} }
} }
pub fn plugin_install(id: String, b: bool) { pub fn plugin_install(_id: String, _b: bool) {
#[cfg(feature = "plugin_framework")] #[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
if b { if _b {
if let Err(e) = crate::plugin::install_plugin(&id) { if let Err(e) = crate::plugin::install_plugin(&id) {
log::error!("Failed to install plugin '{}': {}", id, e); log::error!("Failed to install plugin '{}': {}", id, e);
} }
} else { } else {
crate::plugin::uninstall_plugin(&id, true); crate::plugin::uninstall_plugin(&_id, true);
} }
} }
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "启用"), ("Enable", "启用"),
("Disable", "禁用"), ("Disable", "禁用"),
("Options", "选项"), ("Options", "选项"),
("resolution_original_tip", "原始分辨率"),
("resolution_fit_local_tip", "适应本地分辨率"),
("resolution_custom_tip", "自定义分辨率"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Aktivieren"), ("Enable", "Aktivieren"),
("Disable", "Deaktivieren"), ("Disable", "Deaktivieren"),
("Options", "Einstellungen"), ("Options", "Einstellungen"),
("resolution_original_tip", "Originalauflösung"),
("resolution_fit_local_tip", "Lokale Auflösung anpassen"),
("resolution_custom_tip", "Benutzerdefinierte Auflösung"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."), ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."),
("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."),
("identical_file_tip", "This file is identical with the peer's one."), ("identical_file_tip", "This file is identical with the peer's one."),
("show_monitors_tip", "Show monitors in toolbar."), ("show_monitors_tip", "Show monitors in toolbar"),
("enter_rustdesk_passwd_tip", "Enter RustDesk password"), ("enter_rustdesk_passwd_tip", "Enter RustDesk password"),
("remember_rustdesk_passwd_tip", "Remember RustDesk password"), ("remember_rustdesk_passwd_tip", "Remember RustDesk password"),
("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"), ("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"),
@ -66,5 +66,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("xorg_not_found_text_tip", "Please install Xorg"), ("xorg_not_found_text_tip", "Please install Xorg"),
("no_desktop_title_tip", "No desktop is available"), ("no_desktop_title_tip", "No desktop is available"),
("no_desktop_text_tip", "Please install GNOME desktop"), ("no_desktop_text_tip", "Please install GNOME desktop"),
("resolution_original_tip", "Original resolution"),
("resolution_fit_local_tip", "Fit local resolution"),
("resolution_custom_tip", "Custom resolution"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Habilitar"), ("Enable", "Habilitar"),
("Disable", "Inhabilitar"), ("Disable", "Inhabilitar"),
("Options", "Opciones"), ("Options", "Opciones"),
("resolution_original_tip", "Resolución original"),
("resolution_fit_local_tip", "Ajustar resolución local"),
("resolution_custom_tip", "Resolución personalizada"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "فعال کردن"), ("Enable", "فعال کردن"),
("Disable", "غیر فعال کردن"), ("Disable", "غیر فعال کردن"),
("Options", "گزینه ها"), ("Options", "گزینه ها"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -75,7 +75,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you want to enter again?", "Voulez-vous participer à nouveau ?"), ("Do you want to enter again?", "Voulez-vous participer à nouveau ?"),
("Connection Error", "Erreur de connexion"), ("Connection Error", "Erreur de connexion"),
("Error", "Erreur"), ("Error", "Erreur"),
("Reset by the peer", "La connexion a été fermée par le pair"), ("Reset by the peer", "La connexion a été fermée par la machine distante"),
("Connecting...", "Connexion..."), ("Connecting...", "Connexion..."),
("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."), ("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."),
("Please try 1 minute later", "Réessayez dans une minute"), ("Please try 1 minute later", "Réessayez dans une minute"),
@ -210,7 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Settings", "Paramètres"), ("Settings", "Paramètres"),
("Username", " Nom d'utilisateur"), ("Username", " Nom d'utilisateur"),
("Invalid port", "Port invalide"), ("Invalid port", "Port invalide"),
("Closed manually by the peer", "Fermé manuellement par le pair"), ("Closed manually by the peer", "Fermé manuellement par la machine distante"),
("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"),
("Run without install", "Exécuter sans installer"), ("Run without install", "Exécuter sans installer"),
("Connect via relay", "Connexion via relais"), ("Connect via relay", "Connexion via relais"),
@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."), ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."),
("Remote ID", "ID de l'appareil à distance"), ("Remote ID", "ID de l'appareil à distance"),
("Paste", "Coller"), ("Paste", "Coller"),
("Paste here?", "Coller ici ?"), ("Paste here?", "Coller ici?"),
("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion?"), ("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion?"),
("Download new version", "Télécharger la nouvelle version"), ("Download new version", "Télécharger la nouvelle version"),
("Touch mode", "Mode tactile"), ("Touch mode", "Mode tactile"),
@ -300,11 +300,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Succeeded", "Succès"), ("Succeeded", "Succès"),
("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"), ("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"),
("Unsupported", "Non pris en charge"), ("Unsupported", "Non pris en charge"),
("Peer denied", "Pair refusé"), ("Peer denied", "Machine distante refusée"),
("Please install plugins", "Veuillez installer les plugins"), ("Please install plugins", "Veuillez installer les plugins"),
("Peer exit", "Sortie des pairs"), ("Peer exit", ""),
("Failed to turn off", "Échec de la désactivation"), ("Failed to turn off", "Échec de la désactivation"),
("Turned off", "Éteindre"), ("Turned off", "Désactivé"),
("In privacy mode", "en mode privé"), ("In privacy mode", "en mode privé"),
("Out privacy mode", "hors mode de confidentialité"), ("Out privacy mode", "hors mode de confidentialité"),
("Language", "Langue"), ("Language", "Langue"),
@ -366,7 +366,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Deny remote access", "Interdir l'accès distant"), ("Deny remote access", "Interdir l'accès distant"),
("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"),
("Network", "Réseau"), ("Network", "Réseau"),
("Enable RDP", "Activer RDP"), ("Enable RDP", "Activer connection RDP"),
("Pin menubar", "Épingler la barre de menus"), ("Pin menubar", "Épingler la barre de menus"),
("Unpin menubar", "Détacher la barre de menu"), ("Unpin menubar", "Détacher la barre de menu"),
("Recording", "Enregistrement"), ("Recording", "Enregistrement"),
@ -377,8 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop session recording", "Stopper l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"),
("Enable Recording Session", "Activer l'enregistrement de session"), ("Enable Recording Session", "Activer l'enregistrement de session"),
("Allow recording session", "Autoriser l'enregistrement de session"), ("Allow recording session", "Autoriser l'enregistrement de session"),
("Enable LAN Discovery", "Activer la découverte réseau local"), ("Enable LAN Discovery", "Activer la découverte sur réseau local"),
("Deny LAN Discovery", "Interdir la découverte réseau local"), ("Deny LAN Discovery", "Interdir la découverte sur réseau local"),
("Write a message", "Ecrire un message"), ("Write a message", "Ecrire un message"),
("Prompt", ""), ("Prompt", ""),
("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."), ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."),
@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."),
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."),
("JumpLink", "Afficher"), ("JumpLink", "Afficher"),
("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (opérer du côté pair)."), ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté machine distante)."),
("Show RustDesk", "Afficher RustDesk"), ("Show RustDesk", "Afficher RustDesk"),
("This PC", "Ce PC"), ("This PC", "Ce PC"),
("or", "ou"), ("or", "ou"),
@ -478,33 +478,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty Password", "Mot de passe non spécifié"), ("Empty Password", "Mot de passe non spécifié"),
("Me", "Moi"), ("Me", "Moi"),
("identical_file_tip", "Ce fichier est identique à celui du pair."), ("identical_file_tip", "Ce fichier est identique à celui du pair."),
("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils."), ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils"),
("View Mode", "Mode vue"), ("View Mode", "Mode vue"),
("login_linux_tip", "Se connecter au compte Linux distant"), ("login_linux_tip", "Se connecter au compte Linux distant"),
("verify_rustdesk_password_tip", ""), ("verify_rustdesk_password_tip", "Vérifier le mot de passe RustDesk"),
("remember_account_tip", ""), ("remember_account_tip", "Se souvenir de ce compte"),
("os_account_desk_tip", ""), ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d'exploitation distant et activer la session de bureau en mode sans affichage"),
("OS Account", ""), ("OS Account", "Compte système d'exploitation"),
("another_user_login_title_tip", ""), ("another_user_login_title_tip", "Un autre utilisateur est déjà connecté"),
("another_user_login_text_tip", ""), ("another_user_login_text_tip", "Déconnexion"),
("xorg_not_found_title_tip", ""), ("xorg_not_found_title_tip", "Xorg introuvable"),
("xorg_not_found_text_tip", ""), ("xorg_not_found_text_tip", "Veuillez installer Xorg"),
("no_desktop_title_tip", ""), ("no_desktop_title_tip", "Aucun gestionaire de bureau n'est disponible"),
("no_desktop_text_tip", ""), ("no_desktop_text_tip", "Veuillez installer le gestionaire de bureau GNOME"),
("No need to elevate", ""), ("No need to elevate", "Pas besoin de permissions administrateur"),
("System Sound", ""), ("System Sound", "Son système"),
("Default", ""), ("Default", "Défaut"),
("New RDP", ""), ("New RDP", "Nouvel RDP"),
("Fingerprint", ""), ("Fingerprint", "Empreinte digitale"),
("Copy Fingerprint", ""), ("Copy Fingerprint", "Copier empreinte digitale"),
("no fingerprints", ""), ("no fingerprints", "Pas d'empreintes digitales"),
("Select a peer", ""), ("Select a peer", "Sélectionnez la machine distante"),
("Select peers", ""), ("Select peers", "Sélectionnez des machines distantes"),
("Plugins", ""), ("Plugins", "Plugins"),
("Uninstall", ""), ("Uninstall", "Désinstaller"),
("Update", ""), ("Update", "Mise à jour"),
("Enable", ""), ("Enable", "Activé"),
("Disable", ""), ("Disable", "Desactivé"),
("Options", ""), ("Options", "Options"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Abilita"), ("Enable", "Abilita"),
("Disable", "Disabilita"), ("Disable", "Disabilita"),
("Options", "Opzioni"), ("Options", "Opzioni"),
("resolution_original_tip", "Risoluzione originale"),
("resolution_fit_local_tip", "Adatta risoluzione locale"),
("resolution_custom_tip", "Risoluzione personalizzata"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Activeer"), ("Enable", "Activeer"),
("Disable", "Deactiveer"), ("Disable", "Deactiveer"),
("Options", "Opties"), ("Options", "Opties"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Włącz"), ("Enable", "Włącz"),
("Disable", "Wyłącz"), ("Disable", "Wyłącz"),
("Options", "Opcje"), ("Options", "Opcje"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -478,7 +478,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty Password", "Senha Vazia"), ("Empty Password", "Senha Vazia"),
("Me", "Eu"), ("Me", "Eu"),
("identical_file_tip", "Este arquivo é idêntico ao do parceiro."), ("identical_file_tip", "Este arquivo é idêntico ao do parceiro."),
("show_monitors_tip", "Mostrar monitores na barra de ferramentas."), ("show_monitors_tip", "Mostrar monitores na barra de ferramentas"),
("View Mode", "Modo de Visualização"), ("View Mode", "Modo de Visualização"),
("login_linux_tip", "Você precisa fazer login na conta Linux remota para habilitar uma sessão de desktop X"), ("login_linux_tip", "Você precisa fazer login na conta Linux remota para habilitar uma sessão de desktop X"),
("verify_rustdesk_password_tip", "Verifique a senha do RustDesk"), ("verify_rustdesk_password_tip", "Verifique a senha do RustDesk"),
@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Habilitar"), ("Enable", "Habilitar"),
("Disable", "Desabilitar"), ("Disable", "Desabilitar"),
("Options", "Opções"), ("Options", "Opções"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", "Включить"), ("Enable", "Включить"),
("Disable", "Отключить"), ("Disable", "Отключить"),
("Options", "Настройки"), ("Options", "Настройки"),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable", ""), ("Enable", ""),
("Disable", ""), ("Disable", ""),
("Options", ""), ("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -774,6 +774,12 @@ pub fn resolutions(name: &str) -> Vec<Resolution> {
Virtual2 disconnected (normal left inverted right x axis y axis) Virtual2 disconnected (normal left inverted right x axis y axis)
Virtual3 disconnected (normal left inverted right x axis y axis) Virtual3 disconnected (normal left inverted right x axis y axis)
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 193mm
1920x1080 60.01*+ 60.01 59.97 59.96 59.93
1680x1050 59.95 59.88
1600x1024 60.17
XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
HDMI-0 connected (normal left inverted right x axis y axis) HDMI-0 connected (normal left inverted right x axis y axis)
@ -783,7 +789,7 @@ pub fn resolutions(name: &str) -> Vec<Resolution> {
if let Some(caps) = re.captures(&xrandr_output) { if let Some(caps) = re.captures(&xrandr_output) {
if let Some(resolutions) = caps.name("resolutions") { if let Some(resolutions) = caps.name("resolutions") {
let resolution_pat = let resolution_pat =
r"\s*(?P<width>\d+)x(?P<height>\d+)\s+(?P<rates>(\d+\.\d+[* ]*)+)\s*\n"; r"\s*(?P<width>\d+)x(?P<height>\d+)\s+(?P<rates>(\d+\.\d+\D*)+)\s*\n";
let resolution_re = Regex::new(&format!(r"{}", resolution_pat)).unwrap(); let resolution_re = Regex::new(&format!(r"{}", resolution_pat)).unwrap();
for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) { for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) {
if let Some((width, height)) = if let Some((width, height)) =

View File

@ -1831,21 +1831,25 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> {
Ok(()) Ok(())
} }
#[inline]
fn str_to_device_name(name: &str) -> [u16; 32] {
let mut device_name: Vec<u16> = wide_string(name);
if device_name.len() < 32 {
device_name.resize(32, 0);
}
let mut result = [0; 32];
result.copy_from_slice(&device_name[..32]);
result
}
pub fn resolutions(name: &str) -> Vec<Resolution> { pub fn resolutions(name: &str) -> Vec<Resolution> {
unsafe { unsafe {
let mut dm: DEVMODEW = std::mem::zeroed(); let mut dm: DEVMODEW = std::mem::zeroed();
let wname = wide_string(name);
let len = if wname.len() <= dm.dmDeviceName.len() {
wname.len()
} else {
dm.dmDeviceName.len()
};
std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len);
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
let mut v = vec![]; let mut v = vec![];
let mut num = 0; let mut num = 0;
let device_name = str_to_device_name(name);
loop { loop {
if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { if EnumDisplaySettingsW(device_name.as_ptr(), num, &mut dm) == 0 {
break; break;
} }
let r = Resolution { let r = Resolution {
@ -1866,8 +1870,8 @@ pub fn current_resolution(name: &str) -> ResultType<Resolution> {
unsafe { unsafe {
let mut dm: DEVMODEW = std::mem::zeroed(); let mut dm: DEVMODEW = std::mem::zeroed();
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _; dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
let wname = wide_string(name); let device_name = str_to_device_name(name);
if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { if EnumDisplaySettingsW(device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 {
bail!( bail!(
"failed to get currrent resolution, errno={}", "failed to get currrent resolution, errno={}",
GetLastError() GetLastError()
@ -1882,25 +1886,26 @@ pub fn current_resolution(name: &str) -> ResultType<Resolution> {
} }
} }
pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
let device_name = str_to_device_name(name);
unsafe { unsafe {
let mut dm: DEVMODEW = std::mem::zeroed(); let mut dm: DEVMODEW = std::mem::zeroed();
if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { if FALSE == EnumDisplaySettingsW(device_name.as_ptr() as _, ENUM_CURRENT_SETTINGS, &mut dm)
{
bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); bail!("EnumDisplaySettingsW failed, errno={}", GetLastError());
} }
let wname = wide_string(name); // dmPelsWidth and dmPelsHeight is the same to width and height
let len = if wname.len() <= dm.dmDeviceName.len() { // Because this process is running in dpi awareness mode.
wname.len() if dm.dmPelsWidth == width as u32 && dm.dmPelsHeight == height as u32 {
} else { return Ok(());
dm.dmDeviceName.len() }
};
std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len);
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
dm.dmPelsWidth = width as _; dm.dmPelsWidth = width as _;
dm.dmPelsHeight = height as _; dm.dmPelsHeight = height as _;
dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH;
let res = ChangeDisplaySettingsExW( let res = ChangeDisplaySettingsExW(
wname.as_ptr(), device_name.as_ptr(),
&mut dm, &mut dm,
NULL as _, NULL as _,
CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET,

View File

@ -8,12 +8,10 @@ use std::{
use bytes::Bytes; use bytes::Bytes;
pub use connection::*; pub use connection::*;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::config::Config2;
use hbb_common::tcp::{self, new_listener}; use hbb_common::tcp::{self, new_listener};
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
anyhow::{anyhow, Context}, anyhow::Context,
bail, bail,
config::{Config, CONNECT_TIMEOUT, RELAY_PORT}, config::{Config, CONNECT_TIMEOUT, RELAY_PORT},
log, log,
@ -25,6 +23,8 @@ use hbb_common::{
timeout, tokio, ResultType, Stream, timeout, tokio, ResultType, Stream,
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::{anyhow::anyhow, config::Config2};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use service::ServiceTmpl; use service::ServiceTmpl;
use service::{GenericService, Service, Subscriber}; use service::{GenericService, Service, Subscriber};

View File

@ -179,8 +179,6 @@ pub struct Connection {
#[cfg(windows)] #[cfg(windows)]
portable: PortableState, portable: PortableState,
from_switch: bool, from_switch: bool,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
origin_resolution: HashMap<String, Resolution>,
voice_call_request_timestamp: Option<NonZeroI64>, voice_call_request_timestamp: Option<NonZeroI64>,
audio_input_device_before_voice_call: Option<String>, audio_input_device_before_voice_call: Option<String>,
options_in_login: Option<OptionMessage>, options_in_login: Option<OptionMessage>,
@ -306,8 +304,6 @@ impl Connection {
#[cfg(windows)] #[cfg(windows)]
portable: Default::default(), portable: Default::default(),
from_switch: false, from_switch: false,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
origin_resolution: Default::default(),
audio_sender: None, audio_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
audio_input_device_before_voice_call: None, audio_input_device_before_voice_call: None,
@ -631,15 +627,18 @@ impl Connection {
conn.post_conn_audit(json!({ conn.post_conn_audit(json!({
"action": "close", "action": "close",
})); }));
#[cfg(not(any(target_os = "android", target_os = "ios")))] let mut active_conns_lock = ALIVE_CONNS.lock().unwrap();
conn.reset_resolution(); active_conns_lock.retain(|&c| c != id);
ALIVE_CONNS.lock().unwrap().retain(|&c| c != id);
if let Some(s) = conn.server.upgrade() { if let Some(s) = conn.server.upgrade() {
let mut s = s.write().unwrap(); let mut s = s.write().unwrap();
s.remove_connection(&conn.inner); s.remove_connection(&conn.inner);
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
try_stop_record_cursor_pos(); try_stop_record_cursor_pos();
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if active_conns_lock.is_empty() {
video_service::reset_resolutions();
}
log::info!("#{} connection loop exited", id); log::info!("#{} connection loop exited", id);
} }
@ -1044,6 +1043,8 @@ impl Connection {
}) })
.into(); .into();
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
video_service::try_reset_current_display();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
pi.resolutions = Some(SupportedResolutions { pi.resolutions = Some(SupportedResolutions {
resolutions: video_service::get_current_display_name() resolutions: video_service::get_current_display_name()
@ -1893,25 +1894,7 @@ impl Connection {
} }
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::ChangeResolution(r)) => { Some(misc::Union::ChangeResolution(r)) => self.change_resolution(&r),
if self.keyboard {
if let Ok(name) = video_service::get_current_display_name() {
if let Ok(current) = crate::platform::current_resolution(&name) {
if let Err(e) = crate::platform::change_resolution(
&name,
r.width as _,
r.height as _,
) {
log::error!("change resolution failed:{:?}", e);
} else {
if !self.origin_resolution.contains_key(&name) {
self.origin_resolution.insert(name, current);
}
}
}
}
}
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::PluginRequest(p)) => { Some(misc::Union::PluginRequest(p)) => {
@ -1953,6 +1936,35 @@ impl Connection {
true true
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn change_resolution(&mut self, r: &Resolution) {
if self.keyboard {
if let Ok(name) = video_service::get_current_display_name() {
#[cfg(all(windows, feature = "virtual_display_driver"))]
if let Some(_ok) =
crate::virtual_display_manager::change_resolution_if_is_virtual_display(
&name,
r.width as _,
r.height as _,
)
{
return;
}
if let Err(e) =
crate::platform::change_resolution(&name, r.width as _, r.height as _)
{
log::error!(
"Failed to change resolution '{}' to ({},{}):{:?}",
&name,
r.width,
r.height,
e
);
}
}
}
}
pub async fn handle_voice_call(&mut self, accepted: bool) { pub async fn handle_voice_call(&mut self, accepted: bool) {
if let Some(ts) = self.voice_call_request_timestamp.take() { if let Some(ts) = self.voice_call_request_timestamp.take() {
let msg = new_voice_call_response(ts.get(), accepted); let msg = new_voice_call_response(ts.get(), accepted);
@ -2147,6 +2159,12 @@ impl Connection {
} }
} }
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(custom_resolution) = o.custom_resolution.as_ref() {
if custom_resolution.width > 0 && custom_resolution.height > 0 {
self.change_resolution(&custom_resolution);
}
}
if self.keyboard { if self.keyboard {
if let Ok(q) = o.block_input.enum_value() { if let Ok(q) = o.block_input.enum_value() {
match q { match q {
@ -2262,20 +2280,6 @@ impl Connection {
} }
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn reset_resolution(&self) {
self.origin_resolution
.iter()
.map(|(name, r)| {
if let Err(e) =
crate::platform::change_resolution(&name, r.width as _, r.height as _)
{
log::error!("change resolution failed:{:?}", e);
}
})
.count();
}
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn release_pressed_modifiers(&mut self) { fn release_pressed_modifiers(&mut self) {
for modifier in self.pressed_modifiers.iter() { for modifier in self.pressed_modifiers.iter() {

View File

@ -25,9 +25,12 @@ use crate::virtual_display_manager;
use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; use crate::{platform::windows::is_process_consent_running, privacy_win_mag};
#[cfg(windows)] #[cfg(windows)]
use hbb_common::get_version_number; use hbb_common::get_version_number;
use hbb_common::tokio::sync::{ use hbb_common::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, protobuf::MessageField,
Mutex as TokioMutex, tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex,
},
}; };
#[cfg(not(windows))] #[cfg(not(windows))]
use scrap::Capturer; use scrap::Capturer;
@ -62,8 +65,70 @@ lazy_static::lazy_static! {
pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default(); pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default();
pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default();
pub static ref LAST_SYNC_DISPLAYS: Arc<RwLock<Vec<DisplayInfo>>> = Default::default(); pub static ref LAST_SYNC_DISPLAYS: Arc<RwLock<Vec<DisplayInfo>>> = Default::default();
static ref ORIGINAL_RESOLUTIONS: Arc<RwLock<HashMap<String, (i32, i32)>>> = Default::default();
} }
// Not virtual display
#[inline]
fn set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) {
let mut original_resolutions = ORIGINAL_RESOLUTIONS.write().unwrap();
match original_resolutions.get(display_name) {
Some(r) => r.clone(),
None => {
original_resolutions.insert(display_name.to_owned(), wh.clone());
wh
}
}
}
// Not virtual display
#[inline]
fn get_original_resolution_(display_name: &str) -> Option<(i32, i32)> {
ORIGINAL_RESOLUTIONS
.read()
.unwrap()
.get(display_name)
.map(|r| r.clone())
}
// Not virtual display
#[inline]
fn get_or_set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) {
let r = get_original_resolution_(display_name);
if let Some(r) = r {
return r;
}
set_original_resolution_(display_name, wh)
}
// Not virtual display
#[inline]
fn update_get_original_resolution_(display_name: &str, w: usize, h: usize) -> Resolution {
let wh = get_or_set_original_resolution_(display_name, (w as _, h as _));
Resolution {
width: wh.0,
height: wh.1,
..Default::default()
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn reset_resolutions() {
for (name, (w, h)) in ORIGINAL_RESOLUTIONS.read().unwrap().iter() {
if let Err(e) = crate::platform::change_resolution(name, *w as _, *h as _) {
log::error!(
"Failed to reset resolution of display '{}' to ({},{}): {}",
name,
w,
h,
e
);
}
}
}
#[inline]
fn is_capturer_mag_supported() -> bool { fn is_capturer_mag_supported() -> bool {
#[cfg(windows)] #[cfg(windows)]
return scrap::CapturerMag::is_supported(); return scrap::CapturerMag::is_supported();
@ -71,22 +136,27 @@ fn is_capturer_mag_supported() -> bool {
false false
} }
#[inline]
pub fn capture_cursor_embedded() -> bool { pub fn capture_cursor_embedded() -> bool {
scrap::is_cursor_embedded() scrap::is_cursor_embedded()
} }
#[inline]
pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option<Instant>) { pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option<Instant>) {
FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap()
} }
#[inline]
pub fn set_privacy_mode_conn_id(conn_id: i32) { pub fn set_privacy_mode_conn_id(conn_id: i32) {
*PRIVACY_MODE_CONN_ID.lock().unwrap() = conn_id *PRIVACY_MODE_CONN_ID.lock().unwrap() = conn_id
} }
#[inline]
pub fn get_privacy_mode_conn_id() -> i32 { pub fn get_privacy_mode_conn_id() -> i32 {
*PRIVACY_MODE_CONN_ID.lock().unwrap() *PRIVACY_MODE_CONN_ID.lock().unwrap()
} }
#[inline]
pub fn is_privacy_mode_supported() -> bool { pub fn is_privacy_mode_supported() -> bool {
#[cfg(windows)] #[cfg(windows)]
return *IS_CAPTURER_MAGNIFIER_SUPPORTED return *IS_CAPTURER_MAGNIFIER_SUPPORTED
@ -491,6 +561,8 @@ fn run(sp: GenericService) -> ResultType<()> {
if *SWITCH.lock().unwrap() { if *SWITCH.lock().unwrap() {
log::debug!("Broadcasting display switch"); log::debug!("Broadcasting display switch");
let mut misc = Misc::new(); let mut misc = Misc::new();
let display_name = get_current_display_name().unwrap_or_default();
let original_resolution = get_original_resolution(&display_name, c.width, c.height);
misc.set_switch_display(SwitchDisplay { misc.set_switch_display(SwitchDisplay {
display: c.current as _, display: c.current as _,
x: c.origin.0 as _, x: c.origin.0 as _,
@ -500,12 +572,15 @@ fn run(sp: GenericService) -> ResultType<()> {
cursor_embedded: capture_cursor_embedded(), cursor_embedded: capture_cursor_embedded(),
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
resolutions: Some(SupportedResolutions { resolutions: Some(SupportedResolutions {
resolutions: get_current_display_name() resolutions: if display_name.is_empty() {
.map(|name| crate::platform::resolutions(&name)) vec![]
.unwrap_or(vec![]), } else {
crate::platform::resolutions(&display_name)
},
..SupportedResolutions::default() ..SupportedResolutions::default()
}) })
.into(), .into(),
original_resolution,
..Default::default() ..Default::default()
}); });
let mut msg_out = Message::new(); let mut msg_out = Message::new();
@ -820,6 +895,24 @@ pub fn handle_one_frame_encoded(
Ok(send_conn_ids) Ok(send_conn_ids)
} }
#[inline]
fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageField<Resolution> {
#[cfg(all(windows, feature = "virtual_display_driver"))]
let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name);
#[cfg(not(all(windows, feature = "virtual_display_driver")))]
let is_virtual_display = false;
Some(if is_virtual_display {
Resolution {
width: 0,
height: 0,
..Default::default()
}
} else {
update_get_original_resolution_(&display_name, w, h)
})
.into()
}
pub(super) fn get_displays_2(all: &Vec<Display>) -> (usize, Vec<DisplayInfo>) { pub(super) fn get_displays_2(all: &Vec<Display>) -> (usize, Vec<DisplayInfo>) {
let mut displays = Vec::new(); let mut displays = Vec::new();
let mut primary = 0; let mut primary = 0;
@ -827,14 +920,17 @@ pub(super) fn get_displays_2(all: &Vec<Display>) -> (usize, Vec<DisplayInfo>) {
if d.is_primary() { if d.is_primary() {
primary = i; primary = i;
} }
let display_name = d.name();
let original_resolution = get_original_resolution(&display_name, d.width(), d.height());
displays.push(DisplayInfo { displays.push(DisplayInfo {
x: d.origin().0 as _, x: d.origin().0 as _,
y: d.origin().1 as _, y: d.origin().1 as _,
width: d.width() as _, width: d.width() as _,
height: d.height() as _, height: d.height() as _,
name: d.name(), name: display_name,
online: d.is_online(), online: d.is_online(),
cursor_embedded: false, cursor_embedded: false,
original_resolution,
..Default::default() ..Default::default()
}); });
} }
@ -853,6 +949,15 @@ pub fn is_inited_msg() -> Option<Message> {
None None
} }
// switch to primary display if long time (30 seconds) no users
#[inline]
pub fn try_reset_current_display() {
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
}
*LAST_ACTIVE.lock().unwrap() = time::Instant::now();
}
pub async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> { pub async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -860,10 +965,6 @@ pub async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
return super::wayland::get_displays().await; return super::wayland::get_displays().await;
} }
} }
// switch to primary display if long time (30 seconds) no users
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
}
Ok(get_displays_2(&try_get_displays()?)) Ok(get_displays_2(&try_get_displays()?))
} }
@ -957,6 +1058,8 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> {
get_current_display_2(try_get_displays()?) get_current_display_2(try_get_displays()?)
} }
// `try_reset_current_display` is needed because `get_displays` may change the current display,
// which may cause the mismatch of current display and the current display name.
pub fn get_current_display_name() -> ResultType<String> { pub fn get_current_display_name() -> ResultType<String> {
Ok(get_current_display_2(try_get_displays()?)?.2.name()) Ok(get_current_display_2(try_get_displays()?)?.2.name())
} }

View File

@ -88,10 +88,7 @@ impl<T: InvokeUiSession> Session<T> {
} }
pub fn is_port_forward(&self) -> bool { pub fn is_port_forward(&self) -> bool {
let conn_type = self.lc let conn_type = self.lc.read().unwrap().conn_type;
.read()
.unwrap()
.conn_type;
conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP
} }
@ -832,6 +829,11 @@ impl<T: InvokeUiSession> Session<T> {
} }
} }
#[inline]
pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) {
self.lc.write().unwrap().set_custom_resolution(wh);
}
pub fn change_resolution(&self, width: i32, height: i32) { pub fn change_resolution(&self, width: i32, height: i32) {
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_change_resolution(Resolution { misc.set_change_resolution(Resolution {

View File

@ -1,6 +1,6 @@
use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; use hbb_common::{allow_err, bail, lazy_static, log, ResultType};
use std::{ use std::{
collections::HashSet, collections::{HashMap, HashSet},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -16,8 +16,8 @@ lazy_static::lazy_static! {
#[derive(Default)] #[derive(Default)]
struct VirtualDisplayManager { struct VirtualDisplayManager {
headless_index: Option<u32>, headless_index_name: Option<(u32, String)>,
peer_required_indices: HashSet<u32>, peer_index_name: HashMap<u32, String>,
} }
impl VirtualDisplayManager { impl VirtualDisplayManager {
@ -54,14 +54,16 @@ pub fn plug_in_headless() -> ResultType<()> {
height: 1080, height: 1080,
sync: 60, sync: 60,
}]; }];
let device_names = windows::get_device_names();
VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?; VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?;
manager.headless_index = Some(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS); let device_name = get_new_device_name(&device_names);
manager.headless_index_name = Some((VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, device_name));
Ok(()) Ok(())
} }
pub fn plug_out_headless() -> bool { pub fn plug_out_headless() -> bool {
let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap();
if let Some(index) = manager.headless_index.take() { if let Some((index, _)) = manager.headless_index_name.take() {
if let Err(e) = virtual_display::plug_out_monitor(index) { if let Err(e) = virtual_display::plug_out_monitor(index) {
log::error!("Plug out monitor failed {}", e); log::error!("Plug out monitor failed {}", e);
} }
@ -71,19 +73,39 @@ pub fn plug_out_headless() -> bool {
} }
} }
pub fn plug_in_peer_required( fn get_new_device_name(device_names: &HashSet<String>) -> String {
modes: Vec<Vec<virtual_display::MonitorMode>>, for _ in 0..3 {
) -> ResultType<Vec<u32>> { let device_names_af = windows::get_device_names();
let diff_names: Vec<_> = device_names_af.difference(&device_names).collect();
if diff_names.len() == 1 {
return diff_names[0].clone();
} else if diff_names.len() > 1 {
log::error!(
"Failed to get diff device names after plugin virtual display, more than one diff names: {:?}",
&diff_names
);
return "".to_string();
}
// Sleep is needed here to wait for the virtual display to be ready.
std::thread::sleep(std::time::Duration::from_millis(50));
}
log::error!("Failed to get diff device names after plugin virtual display",);
"".to_string()
}
pub fn plug_in_peer_request(modes: Vec<Vec<virtual_display::MonitorMode>>) -> ResultType<Vec<u32>> {
let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap();
VirtualDisplayManager::prepare_driver()?; VirtualDisplayManager::prepare_driver()?;
let mut indices: Vec<u32> = Vec::new(); let mut indices: Vec<u32> = Vec::new();
for m in modes.iter() { for m in modes.iter() {
for idx in VIRTUAL_DISPLAY_START_FOR_PEER..VIRTUAL_DISPLAY_MAX_COUNT { for idx in VIRTUAL_DISPLAY_START_FOR_PEER..VIRTUAL_DISPLAY_MAX_COUNT {
if !manager.peer_required_indices.contains(&idx) { if !manager.peer_index_name.contains_key(&idx) {
let device_names = windows::get_device_names();
match VirtualDisplayManager::plug_in_monitor(idx, m) { match VirtualDisplayManager::plug_in_monitor(idx, m) {
Ok(_) => { Ok(_) => {
manager.peer_required_indices.insert(idx); let device_name = get_new_device_name(&device_names);
manager.peer_index_name.insert(idx, device_name);
indices.push(idx); indices.push(idx);
} }
Err(e) => { Err(e) => {
@ -97,13 +119,135 @@ pub fn plug_in_peer_required(
Ok(indices) Ok(indices)
} }
pub fn plug_out_peer_required(modes: &[u32]) -> ResultType<()> { pub fn plug_out_peer_request(modes: &[u32]) -> ResultType<()> {
let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap();
for idx in modes.iter() { for idx in modes.iter() {
if manager.peer_required_indices.contains(idx) { if manager.peer_index_name.contains_key(idx) {
allow_err!(virtual_display::plug_out_monitor(*idx)); allow_err!(virtual_display::plug_out_monitor(*idx));
manager.peer_required_indices.remove(idx); manager.peer_index_name.remove(idx);
} }
} }
Ok(()) Ok(())
} }
pub fn is_virtual_display(name: &str) -> bool {
let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap();
if let Some((_, device_name)) = &lock.headless_index_name {
if windows::is_device_name(device_name, name) {
return true;
}
}
for (k, v) in lock.peer_index_name.iter() {
if windows::is_device_name(v, name) {
return true;
}
}
false
}
fn change_resolution(index: u32, w: u32, h: u32) -> bool {
let modes = [virtual_display::MonitorMode {
width: w,
height: h,
sync: 60,
}];
match virtual_display::update_monitor_modes(index, &modes) {
Ok(_) => true,
Err(e) => {
log::error!("Update monitor {} modes {:?} failed: {}", index, &modes, e);
false
}
}
}
pub fn change_resolution_if_is_virtual_display(name: &str, w: u32, h: u32) -> Option<bool> {
let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap();
if let Some((index, device_name)) = &lock.headless_index_name {
if windows::is_device_name(device_name, name) {
return Some(change_resolution(*index, w, h));
}
}
for (k, v) in lock.peer_index_name.iter() {
if windows::is_device_name(v, name) {
return Some(change_resolution(*k, w, h));
}
}
None
}
mod windows {
use std::{collections::HashSet, ptr::null_mut};
use winapi::{
shared::minwindef::{DWORD, FALSE},
um::{
wingdi::{
DEVMODEW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_MIRRORING_DRIVER,
},
winuser::{EnumDisplayDevicesW, EnumDisplaySettingsExW, ENUM_CURRENT_SETTINGS},
},
};
// This string is defined here.
// https://github.com/fufesou/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40
const IDD_DEVICE_STRING: &'static str = "RustDeskIddDriver Device\0";
#[inline]
pub(super) fn is_device_name(device_name: &str, name: &str) -> bool {
if name.len() == device_name.len() {
name == device_name
} else if name.len() > device_name.len() {
false
} else {
&device_name[..name.len()] == name && device_name.as_bytes()[name.len() as usize] == 0
}
}
pub(super) fn get_device_names() -> HashSet<String> {
let mut device_names = HashSet::new();
let mut dd: DISPLAY_DEVICEW = unsafe { std::mem::zeroed() };
dd.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as DWORD;
let mut i_dev_num = 0;
loop {
let result = unsafe { EnumDisplayDevicesW(null_mut(), i_dev_num, &mut dd, 0) };
if result == 0 {
break;
}
i_dev_num += 1;
if 0 == (dd.StateFlags & DISPLAY_DEVICE_ACTIVE)
|| (dd.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0
{
continue;
}
let mut dm: DEVMODEW = unsafe { std::mem::zeroed() };
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
dm.dmDriverExtra = 0;
let ok = unsafe {
EnumDisplaySettingsExW(
dd.DeviceName.as_ptr(),
ENUM_CURRENT_SETTINGS,
&mut dm as _,
0,
)
};
if ok == FALSE {
continue;
}
if dm.dmPelsHeight == 0 || dm.dmPelsWidth == 0 {
continue;
}
if let (Ok(device_name), Ok(device_string)) = (
String::from_utf16(&dd.DeviceName),
String::from_utf16(&dd.DeviceString),
) {
if &device_string[..IDD_DEVICE_STRING.len()] == IDD_DEVICE_STRING {
device_names.insert(device_name);
}
}
}
device_names
}
}