Merge branch 'master' into hwcodec

This commit is contained in:
21pages 2022-07-06 18:57:14 +08:00
commit 68204e0c56
28 changed files with 417 additions and 105 deletions

View File

@ -202,7 +202,7 @@ target/release/rustdesk
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务audio/clipboard/input/video 服务, 已经连接实现 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务audio/clipboard/input/video 服务, 以及连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继) - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码

View File

@ -43,7 +43,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode|navigation"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@ -310,19 +310,19 @@ class ImageModel with ChangeNotifier {
} }
double get maxScale { double get maxScale {
if (_image == null) return 1.0; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return max(1.0, max(xscale, yscale)); return max(1.5, max(xscale, yscale));
} }
double get minScale { double get minScale {
if (_image == null) return 1.0; if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / _image!.width; final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return min(xscale, yscale); return min(xscale, yscale) / 1.5;
} }
} }
@ -724,13 +724,17 @@ class FFI {
static void inputKey(String name, {bool? down, bool? press}) { static void inputKey(String name, {bool? down, bool? press}) {
if (!ffiModel.keyboard()) return; if (!ffiModel.keyboard()) return;
setByName( final Map<String, String> out = Map();
'input_key', out['name'] = name;
json.encode(modify({ // default: down = false
'name': name, if (down == true) {
'down': (down ?? false).toString(), out['down'] = "true";
'press': (press ?? true).toString() }
}))); // default: press = true
if (press != false) {
out['press'] = "true";
}
setByName('input_key', json.encode(modify(out)));
} }
static void moveMouse(double x, double y) { static void moveMouse(double x, double y) {

View File

@ -343,9 +343,14 @@ class _RemotePageState extends State<RemotePage> {
onKey: (data, e) { onKey: (data, e) {
final key = e.logicalKey; final key = e.logicalKey;
if (e is RawKeyDownEvent) { if (e is RawKeyDownEvent) {
if (e.repeat) { if (e.repeat &&
!e.isAltPressed &&
!e.isControlPressed &&
!e.isShiftPressed &&
!e.isMetaPressed) {
sendRawKey(e, press: true); sendRawKey(e, press: true);
} else { } else {
sendRawKey(e, down: true);
if (e.isAltPressed && !FFI.alt) { if (e.isAltPressed && !FFI.alt) {
FFI.alt = true; FFI.alt = true;
} else if (e.isControlPressed && !FFI.ctrl) { } else if (e.isControlPressed && !FFI.ctrl) {
@ -355,7 +360,6 @@ class _RemotePageState extends State<RemotePage> {
} else if (e.isMetaPressed && !FFI.command) { } else if (e.isMetaPressed && !FFI.command) {
FFI.command = true; FFI.command = true;
} }
sendRawKey(e, down: true);
} }
} }
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
@ -481,6 +485,7 @@ class _RemotePageState extends State<RemotePage> {
/// DoubleFiner -> right click /// DoubleFiner -> right click
/// HoldDrag -> left drag /// HoldDrag -> left drag
Offset _cacheLongPressPosition = Offset(0, 0);
Widget getBodyForMobileWithGesture() { Widget getBodyForMobileWithGesture() {
final touchMode = FFI.ffiModel.touchMode; final touchMode = FFI.ffiModel.touchMode;
return getMixinGestureDetector( return getMixinGestureDetector(
@ -504,10 +509,14 @@ class _RemotePageState extends State<RemotePage> {
}, },
onLongPressDown: (d) { onLongPressDown: (d) {
if (touchMode) { if (touchMode) {
FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); _cacheLongPressPosition = d.localPosition;
} }
}, },
onLongPress: () { onLongPress: () {
if (touchMode) {
FFI.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
FFI.tap(MouseButtons.right); FFI.tap(MouseButtons.right);
}, },
onDoubleFinerTap: (d) { onDoubleFinerTap: (d) {
@ -534,6 +543,15 @@ class _RemotePageState extends State<RemotePage> {
if (touchMode) { if (touchMode) {
FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
FFI.sendMouse('down', MouseButtons.left); FFI.sendMouse('down', MouseButtons.left);
} else {
final cursorX = FFI.cursorModel.x;
final cursorY = FFI.cursorModel.y;
final visible =
FFI.cursorModel.getVisibleRect().inflate(1); // extend edges
final size = MediaQueryData.fromWindow(ui.window).size;
if (!visible.contains(Offset(cursorX, cursorY))) {
FFI.cursorModel.move(size.width / 2, size.height / 2);
}
} }
}, },
onOneFingerPanUpdate: (d) { onOneFingerPanUpdate: (d) {

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum CustomTouchGestureState { enum GestureState {
none, none,
oneFingerPan, oneFingerPan,
twoFingerScale, twoFingerScale,
@ -35,64 +35,41 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate; GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
GestureDragEndCallback? onThreeFingerVerticalDragEnd; GestureDragEndCallback? onThreeFingerVerticalDragEnd;
var _currentState = CustomTouchGestureState.none; var _currentState = GestureState.none;
Timer? _startEventDebounceTimer; Timer? _debounceTimer;
void _init() { void _init() {
debugPrint("CustomTouchGestureRecognizer init"); debugPrint("CustomTouchGestureRecognizer init");
onStart = (d) { // onStart = (d) {};
_startEventDebounceTimer?.cancel(); onUpdate = (d) {
if (d.pointerCount == 1) { _debounceTimer?.cancel();
_currentState = CustomTouchGestureState.oneFingerPan; if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
if (onOneFingerPanStart != null) { onOneFingerStartDebounce(d);
onOneFingerPanStart!(DragStartDetails( } else if (d.pointerCount == 2 &&
localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); _currentState != GestureState.twoFingerScale) {
} onTwoFingerStartDebounce(d);
debugPrint("start oneFingerPan"); } else if (d.pointerCount == 3 &&
} else if (d.pointerCount == 2) { _currentState != GestureState.threeFingerVerticalDrag) {
if (_currentState == CustomTouchGestureState.threeFingerVerticalDrag) { _currentState = GestureState.threeFingerVerticalDrag;
// 3 -> 2 debounce
_startEventDebounceTimer = Timer(Duration(milliseconds: 200), () {
_currentState = CustomTouchGestureState.twoFingerScale;
if (onTwoFingerScaleStart != null) {
onTwoFingerScaleStart!(ScaleStartDetails(
localFocalPoint: d.localFocalPoint,
focalPoint: d.focalPoint));
}
debugPrint("debounce start twoFingerScale success");
});
}
_currentState = CustomTouchGestureState.twoFingerScale;
// startWatchTimer();
if (onTwoFingerScaleStart != null) {
onTwoFingerScaleStart!(ScaleStartDetails(
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
}
debugPrint("start twoFingerScale");
} else if (d.pointerCount == 3) {
_currentState = CustomTouchGestureState.threeFingerVerticalDrag;
if (onThreeFingerVerticalDragStart != null) { if (onThreeFingerVerticalDragStart != null) {
onThreeFingerVerticalDragStart!( onThreeFingerVerticalDragStart!(
DragStartDetails(globalPosition: d.localFocalPoint)); DragStartDetails(globalPosition: d.localFocalPoint));
} }
debugPrint("start threeFingerScale"); debugPrint("start threeFingerScale");
// _reset();
} }
}; if (_currentState != GestureState.none) {
onUpdate = (d) {
if (_currentState != CustomTouchGestureState.none) {
switch (_currentState) { switch (_currentState) {
case CustomTouchGestureState.oneFingerPan: case GestureState.oneFingerPan:
if (onOneFingerPanUpdate != null) { if (onOneFingerPanUpdate != null) {
onOneFingerPanUpdate!(_getDragUpdateDetails(d)); onOneFingerPanUpdate!(_getDragUpdateDetails(d));
} }
break; break;
case CustomTouchGestureState.twoFingerScale: case GestureState.twoFingerScale:
if (onTwoFingerScaleUpdate != null) { if (onTwoFingerScaleUpdate != null) {
onTwoFingerScaleUpdate!(d); onTwoFingerScaleUpdate!(d);
} }
break; break;
case CustomTouchGestureState.threeFingerVerticalDrag: case GestureState.threeFingerVerticalDrag:
if (onThreeFingerVerticalDragUpdate != null) { if (onThreeFingerVerticalDragUpdate != null) {
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d)); onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
} }
@ -105,21 +82,22 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}; };
onEnd = (d) { onEnd = (d) {
debugPrint("ScaleGestureRecognizer onEnd"); debugPrint("ScaleGestureRecognizer onEnd");
_debounceTimer?.cancel();
// end // end
switch (_currentState) { switch (_currentState) {
case CustomTouchGestureState.oneFingerPan: case GestureState.oneFingerPan:
debugPrint("TwoFingerState.pan onEnd"); debugPrint("TwoFingerState.pan onEnd");
if (onOneFingerPanEnd != null) { if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d)); onOneFingerPanEnd!(_getDragEndDetails(d));
} }
break; break;
case CustomTouchGestureState.twoFingerScale: case GestureState.twoFingerScale:
debugPrint("TwoFingerState.scale onEnd"); debugPrint("TwoFingerState.scale onEnd");
if (onTwoFingerScaleEnd != null) { if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d); onTwoFingerScaleEnd!(d);
} }
break; break;
case CustomTouchGestureState.threeFingerVerticalDrag: case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd"); debugPrint("ThreeFingerState.vertical onEnd");
if (onThreeFingerVerticalDragEnd != null) { if (onThreeFingerVerticalDragEnd != null) {
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d)); onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
@ -128,10 +106,50 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
default: default:
break; break;
} }
_currentState = CustomTouchGestureState.none; _debounceTimer = Timer(Duration(milliseconds: 200), () {
_currentState = GestureState.none;
});
}; };
} }
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
final start = (ScaleUpdateDetails d) {
_currentState = GestureState.oneFingerPan;
if (onOneFingerPanStart != null) {
onOneFingerPanStart!(DragStartDetails(
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
}
};
if (_currentState != GestureState.none) {
_debounceTimer = Timer(Duration(milliseconds: 200), () {
start(d);
debugPrint("debounce start oneFingerPan");
});
} else {
start(d);
debugPrint("start oneFingerPan");
}
}
void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
final start = (ScaleUpdateDetails d) {
_currentState = GestureState.twoFingerScale;
if (onTwoFingerScaleStart != null) {
onTwoFingerScaleStart!(ScaleStartDetails(
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
}
};
if (_currentState == GestureState.threeFingerVerticalDrag) {
_debounceTimer = Timer(Duration(milliseconds: 200), () {
start(d);
debugPrint("debounce start twoFingerScale");
});
} else {
start(d);
debugPrint("start twoFingerScale");
}
}
DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) => DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) =>
DragUpdateDetails( DragUpdateDetails(
globalPosition: d.focalPoint, globalPosition: d.focalPoint,

View File

@ -1,31 +1,177 @@
from pynput.keyboard import Key, Controller from pynput.keyboard import Key, Controller
from pynput.keyboard._xorg import KeyCode from pynput.keyboard._xorg import KeyCode
from pynput._util.xorg import display_manager from pynput._util.xorg import display_manager
import Xlib
from pynput._util.xorg import *
import Xlib
import os import os
import sys import sys
import socket import socket
from Xlib.ext.xtest import fake_input
from Xlib import X
import Xlib
KeyCode._from_symbol("\0") # test KeyCode._from_symbol("\0") # test
DEAD_KEYS = {
'`': 65104,
'´': 65105,
'^': 65106,
'~': 65107,
'¯': 65108,
'˘': 65109,
'˙': 65110,
'¨': 65111,
'˚': 65112,
'˝': 65113,
'ˇ': 65114,
'¸': 65115,
'˛': 65116,
'': 65117, # ?
'': 65118, # ?
'': 65119,
'ٜ': 65120,
'': 65121,
' ̛': 65122,
}
def my_keyboard_mapping(display):
"""Generates a mapping from *keysyms* to *key codes* and required
modifier shift states.
:param Xlib.display.Display display: The display for which to retrieve the
keyboard mapping.
:return: the keyboard mapping
"""
mapping = {}
shift_mask = 1 << 0
group_mask = alt_gr_mask(display)
# Iterate over all keysym lists in the keyboard mapping
min_keycode = display.display.info.min_keycode
keycode_count = display.display.info.max_keycode - min_keycode + 1
for index, keysyms in enumerate(display.get_keyboard_mapping(
min_keycode, keycode_count)):
key_code = index + min_keycode
# Normalise the keysym list to yield a tuple containing the two groups
normalized = keysym_normalize(keysyms)
if not normalized:
continue
# Iterate over the groups to extract the shift and modifier state
for groups, group in zip(normalized, (False, True)):
for keysym, shift in zip(groups, (False, True)):
if not keysym:
continue
shift_state = 0 \
| (shift_mask if shift else 0) \
| (group_mask if group else 0)
# !!!: Save all keycode combinations of keysym
if keysym in mapping:
mapping[keysym].append((key_code, shift_state))
else:
mapping[keysym] = [(key_code, shift_state)]
return mapping
class MyController(Controller): class MyController(Controller):
def _handle(self, key, is_press): def _update_keyboard_mapping(self):
"""Resolves a key identifier and sends a keyboard event. """Updates the keyboard mapping.
:param event: The *X* keyboard event.
:param int keysym: The keysym to handle.
""" """
keysym = self._keysym(key) with display_manager(self._display) as dm:
keycode = self._display.keysym_to_keycode(keysym) self._keyboard_mapping = my_keyboard_mapping(dm)
def send_event(self, event, keycode, shift_state):
with display_manager(self._display) as dm, self.modifiers as modifiers:
# Under certain cimcumstances, such as when running under Xephyr,
# the value returned by dm.get_input_focus is an int
window = dm.get_input_focus().focus
send_event = getattr(
window,
'send_event',
lambda event: dm.send_event(window, event))
send_event(event(
detail=keycode,
state=shift_state | self._shift_mask(modifiers),
time=0,
root=dm.screen().root,
window=window,
same_screen=0,
child=Xlib.X.NONE,
root_x=0, root_y=0, event_x=0, event_y=0))
def fake_input(self, keycode, is_press):
with display_manager(self._display) as dm: with display_manager(self._display) as dm:
Xlib.ext.xtest.fake_input( Xlib.ext.xtest.fake_input(
dm, dm,
Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease, Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease,
keycode) keycode)
def _handle(self, key, is_press):
"""Resolves a key identifier and sends a keyboard event.
:param event: The *X* keyboard event.
:param int keysym: The keysym to handle.
"""
event = Xlib.display.event.KeyPress if is_press \
else Xlib.display.event.KeyRelease
keysym = self._keysym(key)
if key.vk is not None:
keycode = self._display.keysym_to_keycode(key.vk)
self.fake_input(keycode, is_press)
# Otherwise use XSendEvent; we need to use this in the general case to
# work around problems with keyboard layouts
self._emit('_on_fake_event', key, is_press)
return
# Make sure to verify that the key was resolved
if keysym is None:
raise self.InvalidKeyException(key)
# There may be multiple keycodes for keysym in keyboard_mapping
keycode_flag = len(self.keyboard_mapping[keysym]) == 1
if keycode_flag:
keycode, shift_state = self.keyboard_mapping[keysym][0]
else:
keycode, shift_state = self._display.keysym_to_keycode(keysym), 0
keycode_set = set(map(lambda x: x[0], self.keyboard_mapping[keysym]))
# The keycode of the dead key is inconsistent, The keysym has multiple combinations of a keycode.
if keycode != self._display.keysym_to_keycode(keysym) \
or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1):
deakkey_chr = str(key).replace("'", '')
keysym = DEAD_KEYS[deakkey_chr]
keycode, shift_state = self.keyboard_mapping[keysym][0]
# If the key has a virtual key code, use that immediately with
# fake_input; fake input,being an X server extension, has access to
# more internal state that we do
try:
with self.modifiers as modifiers:
alt_gr = Key.alt_gr in modifiers
# !!!: Send_event can't support lock screen, this condition cann't be modified
if alt_gr:
self.send_event(
event, keycode, shift_state)
else:
self.fake_input(keycode, is_press)
except KeyError:
with self._borrow_lock:
keycode, index, count = self._borrows[keysym]
self._send_key(
event,
keycode,
index_to_shift(self._display, index))
count += 1 if is_press else -1
self._borrows[keysym] = (keycode, index, count)
# Notify any running listeners
self._emit('_on_fake_event', key, is_press)
keyboard = MyController() keyboard = MyController()
@ -82,7 +228,7 @@ def loop():
else: else:
keyboard.release(name) keyboard.release(name)
except Exception as e: except Exception as e:
print(e) print('[x] error key',e)
loop() loop()

View File

@ -47,7 +47,7 @@ case "$1" in
;; ;;
2) 2)
# for upgrade # for upgrade
service rustdesk stop || true systemctl stop rustdesk || true
;; ;;
esac esac
@ -61,10 +61,26 @@ systemctl start rustdesk
update-desktop-database update-desktop-database
%preun %preun
systemctl stop rustdesk || true case "$1" in
systemctl disable rustdesk || true 0)
rm /etc/systemd/system/rustdesk.service || true # for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun %postun
rm /usr/share/applications/rustdesk.desktop || true case "$1" in
update-desktop-database 0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac

View File

@ -48,7 +48,7 @@ case "$1" in
;; ;;
2) 2)
# for upgrade # for upgrade
service rustdesk stop || true systemctl stop rustdesk || true
;; ;;
esac esac
@ -62,10 +62,26 @@ systemctl start rustdesk
update-desktop-database update-desktop-database
%preun %preun
systemctl stop rustdesk || true case "$1" in
systemctl disable rustdesk || true 0)
rm /etc/systemd/system/rustdesk.service || true # for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun %postun
rm /usr/share/applications/rustdesk.desktop || true case "$1" in
update-desktop-database 0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac

View File

@ -1,21 +1,43 @@
use serde_json::{json, value::Value};
use std::ops::Deref; use std::ops::Deref;
mod cn; mod cn;
mod cs; mod cs;
mod da; mod da;
mod sk;
mod de; mod de;
mod en; mod en;
mod es;
mod eo; mod eo;
mod es;
mod fr; mod fr;
mod id; mod id;
mod it; mod it;
mod ptbr; mod ptbr;
mod ru; mod ru;
mod sk;
mod tr; mod tr;
mod tw; mod tw;
lazy_static::lazy_static! {
pub static ref LANGS: Value =
json!(vec![
("en", "English"),
("it", "Italiano"),
("fr", "Français"),
("de", "Deutsch"),
("cn", "简体中文"),
("tw", "繁體中文"),
("pt", "Português"),
("es", "Español"),
("ru", "Русский"),
("sk", "Slovenčina"),
("id", "Indonesia"),
("cs", "Čeština"),
("da", "Dansk"),
("eo", "Esperanto"),
("tr", "Türkçe"),
]);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String { pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase(); let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase();

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"), ("Turned off", "退出"),
("In privacy mode", "进入隐私模式"), ("In privacy mode", "进入隐私模式"),
("Out privacy mode", "退出隐私模式"), ("Out privacy mode", "退出隐私模式"),
("Language", "语言"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"), ("Turned off", "Vypnutý"),
("In privacy mode", "v režimu soukromí"), ("In privacy mode", "v režimu soukromí"),
("Out privacy mode", "mimo režim soukromí"), ("Out privacy mode", "mimo režim soukromí"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Slukket"), ("Turned off", "Slukket"),
("In privacy mode", "I databeskyttelsestilstand"), ("In privacy mode", "I databeskyttelsestilstand"),
("Out privacy mode", "Databeskyttelsestilstand fra"), ("Out privacy mode", "Databeskyttelsestilstand fra"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Ausgeschaltet"), ("Turned off", "Ausgeschaltet"),
("In privacy mode", "im Datenschutzmodus"), ("In privacy mode", "im Datenschutzmodus"),
("Out privacy mode", "Datenschutzmodus aus"), ("Out privacy mode", "Datenschutzmodus aus"),
("Language", "Sprache"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""), ("Turned off", ""),
("In privacy mode", ""), ("In privacy mode", ""),
("Out privacy mode", ""), ("Out privacy mode", ""),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Apagado"), ("Turned off", "Apagado"),
("In privacy mode", "En modo de privacidad"), ("In privacy mode", "En modo de privacidad"),
("Out privacy mode", "Fuera del modo de privacidad"), ("Out privacy mode", "Fuera del modo de privacidad"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Éteindre"), ("Turned off", "Éteindre"),
("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"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Matikan"), ("Turned off", "Matikan"),
("In privacy mode", "Dalam mode privasi"), ("In privacy mode", "Dalam mode privasi"),
("Out privacy mode", "Keluar dari mode privasi"), ("Out privacy mode", "Keluar dari mode privasi"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Spegni"), ("Turned off", "Spegni"),
("In privacy mode", "In modalità privacy"), ("In privacy mode", "In modalità privacy"),
("Out privacy mode", "Fuori modalità privacy"), ("Out privacy mode", "Fuori modalità privacy"),
("Language", "Linguaggio"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Desligado"), ("Turned off", "Desligado"),
("In privacy mode", "No modo de privacidade"), ("In privacy mode", "No modo de privacidade"),
("Out privacy mode", "Fora do modo de privacidade"), ("Out privacy mode", "Fora do modo de privacidade"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Выключен"), ("Turned off", "Выключен"),
("In privacy mode", "В режиме конфиденциальности"), ("In privacy mode", "В режиме конфиденциальности"),
("Out privacy mode", "Выход из режима конфиденциальности"), ("Out privacy mode", "Выход из режима конфиденциальности"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Vypnutý"), ("Turned off", "Vypnutý"),
("In privacy mode", "V režime súkromia"), ("In privacy mode", "V režime súkromia"),
("Out privacy mode", "Mimo režimu súkromia"), ("Out privacy mode", "Mimo režimu súkromia"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", ""), ("Turned off", ""),
("In privacy mode", ""), ("In privacy mode", ""),
("Out privacy mode", ""), ("Out privacy mode", ""),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "Kapalı"), ("Turned off", "Kapalı"),
("In privacy mode", "Gizlilik modunda"), ("In privacy mode", "Gizlilik modunda"),
("Out privacy mode", "Gizlilik modu dışında"), ("Out privacy mode", "Gizlilik modu dışında"),
("Language", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -283,5 +283,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Turned off", "退出"), ("Turned off", "退出"),
("In privacy mode", "開啟隱私模式"), ("In privacy mode", "開啟隱私模式"),
("Out privacy mode", "退出隱私模式"), ("Out privacy mode", "退出隱私模式"),
("Language", "語言"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -1020,6 +1020,22 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
// https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa // https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa
// https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10 // https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10
// https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html // https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html
// Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895
let dels = format!(
"
if exist \"{mk_shortcut}\" del /f /q \"{mk_shortcut}\"
if exist \"{uninstall_shortcut}\" del /f /q \"{uninstall_shortcut}\"
if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\"
if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\"
if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\"
",
mk_shortcut = mk_shortcut,
uninstall_shortcut = uninstall_shortcut,
tray_shortcut = tray_shortcut,
tmp_path = tmp_path,
app_name = crate::get_app_name(),
);
let cmds = format!( let cmds = format!(
" "
{uninstall_str} {uninstall_str}
@ -1048,17 +1064,13 @@ cscript \"{tray_shortcut}\"
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
{shortcuts} {shortcuts}
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
del /f \"{mk_shortcut}\" {dels}
del /f \"{uninstall_shortcut}\"
del /f \"{tray_shortcut}\"
del /f \"{tmp_path}\\{app_name}.lnk\"
del /f \"{tmp_path}\\Uninstall {app_name}.lnk\"
del /f \"{tmp_path}\\{app_name} Tray.lnk\"
sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\"
sc start {app_name} sc start {app_name}
sc stop {app_name} sc stop {app_name}
sc delete {app_name} sc delete {app_name}
{after_install} {after_install}
{sleep}
", ",
uninstall_str=uninstall_str, uninstall_str=uninstall_str,
path=path, path=path,
@ -1081,6 +1093,16 @@ sc delete {app_name}
config_path=Config::file().to_str().unwrap_or(""), config_path=Config::file().to_str().unwrap_or(""),
lic=register_licence(), lic=register_licence(),
after_install=get_after_install(&exe), after_install=get_after_install(&exe),
sleep=if debug {
"timeout 300"
} else {
""
},
dels=if debug {
""
} else {
&dels
},
); );
run_cmds(cmds, debug, "install")?; run_cmds(cmds, debug, "install")?;
std::thread::sleep(std::time::Duration::from_millis(2000)); std::thread::sleep(std::time::Duration::from_millis(2000));
@ -1126,10 +1148,10 @@ fn get_uninstall() -> String {
" "
{before_uninstall} {before_uninstall}
reg delete {subkey} /f reg delete {subkey} /f
rd /s /q \"{path}\" if exist \"{path}\" rd /s /q \"{path}\"
rd /s /q \"{start_menu}\" if exist \"{start_menu}\" rd /s /q \"{start_menu}\"
del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\" if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\"
del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
", ",
before_uninstall=get_before_uninstall(), before_uninstall=get_before_uninstall(),
subkey=subkey, subkey=subkey,
@ -1176,11 +1198,8 @@ fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
.show(show) .show(show)
.force_prompt(true) .force_prompt(true)
.status(); .status();
// leave the file for debug if execution failed if !show {
if let Ok(res) = res { allow_err!(std::fs::remove_file(tmp));
if res.success() {
allow_err!(std::fs::remove_file(tmp));
}
} }
let _ = res?; let _ = res?;
Ok(()) Ok(())

View File

@ -3,9 +3,9 @@ mod cm;
mod inline; mod inline;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
pub mod remote;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod win_privacy; pub mod win_privacy;
pub mod remote;
use crate::common::SOFTWARE_UPDATE_URL; use crate::common::SOFTWARE_UPDATE_URL;
use crate::ipc; use crate::ipc;
use hbb_common::{ use hbb_common::{
@ -760,6 +760,10 @@ impl UI {
#[cfg(feature = "hwcodec")] #[cfg(feature = "hwcodec")]
return true; return true;
} }
fn get_langs(&self) -> String {
crate::lang::LANGS.to_string()
}
} }
impl sciter::EventHandler for UI { impl sciter::EventHandler for UI {
@ -837,6 +841,7 @@ impl sciter::EventHandler for UI {
fn get_lan_peers(); fn get_lan_peers();
fn get_uuid(); fn get_uuid();
fn has_hwcodec(); fn has_hwcodec();
fn get_langs();
} }
} }

View File

@ -163,6 +163,40 @@ class AudioInputs: Reactor.Component {
} }
this.toggleMenuState(); this.toggleMenuState();
} }
};
class Languages: Reactor.Component {
function render() {
var langs = JSON.parse(handler.get_langs());
var me = this;
self.timer(1ms, function() { me.toggleMenuState() });
return <li>{translate('Language')}
<menu #languages key={langs.length}>
<li id="default"><span>{svg_checkmark}</span>Default</li>
<div .separator />
{langs.map(function(lang) {
return <li id={lang[0]}><span>{svg_checkmark}</span>{lang[1]}</li>;
})}
</menu>
</li>;
}
function toggleMenuState() {
var cur = handler.get_local_option("lang") || "default";
for (var el in this.$$(menu#languages>li)) {
var selected = cur == el.id;
el.attributes.toggleClass("selected", selected);
}
}
event click $(menu#languages>li) (_, me) {
var v = me.id;
if (v == "default") v = "";
handler.set_local_option("lang", v);
app.update();
this.toggleMenuState();
}
} }
var enhancementsMenu; var enhancementsMenu;
@ -202,7 +236,6 @@ class Enhancements: Reactor.Component {
} }
} }
function getUserName() { function getUserName() {
try { try {
return JSON.parse(handler.get_local_option("user_info")).name; return JSON.parse(handler.get_local_option("user_info")).name;
@ -261,7 +294,7 @@ class MyIdMenu: Reactor.Component {
{handler.is_ok_change_id() && key_confirmed ? <li #change-id>{translate('Change ID')}</li> : ""} {handler.is_ok_change_id() && key_confirmed ? <li #change-id>{translate('Change ID')}</li> : ""}
<div .separator /> <div .separator />
<li #allow-darktheme><span>{svg_checkmark}</span>{translate('Dark Theme')}</li> <li #allow-darktheme><span>{svg_checkmark}</span>{translate('Dark Theme')}</li>
<div .separator /> <Languages />
<li #about>{translate('About')} {" "}{handler.get_app_name()}</li> <li #about>{translate('About')} {" "}{handler.get_app_name()}</li>
</menu> </menu>
</popup>; </popup>;

View File

@ -2001,11 +2001,11 @@ impl Remote {
let mut config: PeerConfig = self.handler.load_config(); let mut config: PeerConfig = self.handler.load_config();
let mut transfer_metas = TransferSerde::default(); let mut transfer_metas = TransferSerde::default();
for job in self.read_jobs.iter() { for job in self.read_jobs.iter() {
let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default();
transfer_metas.read_jobs.push(json_str); transfer_metas.read_jobs.push(json_str);
} }
for job in self.write_jobs.iter() { for job in self.write_jobs.iter() {
let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default();
transfer_metas.write_jobs.push(json_str); transfer_metas.write_jobs.push(json_str);
} }
log::info!("meta: {:?}", transfer_metas); log::info!("meta: {:?}", transfer_metas);