Merge branch 'rustdesk:master' into master

This commit is contained in:
Cacing69 2023-08-24 08:02:45 +07:00 committed by GitHub
commit e09b4f878e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 971 additions and 490 deletions

View File

@ -22,7 +22,7 @@ env:
# vcpkg version: 2023.04.15 # vcpkg version: 2023.04.15
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1" VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2" VERSION: "1.2.3"
NDK_VERSION: "r25c" NDK_VERSION: "r25c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'

View File

@ -15,4 +15,4 @@ jobs:
secrets: inherit secrets: inherit
with: with:
upload-artifact: true upload-artifact: true
upload-tag: "1.2.2" upload-tag: "1.2.3"

View File

@ -10,7 +10,7 @@ env:
# vcpkg version: 2022.05.10 # vcpkg version: 2022.05.10
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1" VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2" VERSION: "1.2.3"
jobs: jobs:
build-for-history-windows: build-for-history-windows:

2
Cargo.lock generated
View File

@ -5124,7 +5124,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.2.2" version = "1.2.3"
dependencies = [ dependencies = [
"android_logger", "android_logger",
"arboard", "arboard",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.2" version = "1.2.3"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"

View File

@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.3
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb - bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.2 version: 1.2.3
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@ -12,7 +12,7 @@
"name": "rustdesk", "name": "rustdesk",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk-1.2.2.deb", "bsdtar -zxvf rustdesk-1.2.3.deb",
"tar -xvf ./data.tar.xz", "tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/", "cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
@ -26,7 +26,7 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "../rustdesk-1.2.2.deb" "path": "../rustdesk-1.2.3.deb"
}, },
{ {
"type": "file", "type": "file",

View File

@ -26,6 +26,13 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331 const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963 const val WHEEL_UP = 963
const val TOUCH_SCALE_START = 1
const val TOUCH_SCALE = 2
const val TOUCH_SCALE_END = 3
const val TOUCH_PAN_START = 4
const val TOUCH_PAN_UPDATE = 5
const val TOUCH_PAN_END = 6
const val WHEEL_STEP = 120 const val WHEEL_STEP = 120
const val WHEEL_DURATION = 50L const val WHEEL_DURATION = 50L
const val LONG_TAP_DELAY = 200L const val LONG_TAP_DELAY = 200L
@ -167,6 +174,30 @@ class InputService : AccessibilityService() {
} }
} }
@RequiresApi(Build.VERSION_CODES.N)
fun onTouchInput(mask: Int, _x: Int, _y: Int) {
when (mask) {
TOUCH_PAN_UPDATE -> {
mouseX -= _x * SCREEN_INFO.scale
mouseY -= _y * SCREEN_INFO.scale
mouseX = max(0, mouseX);
mouseY = max(0, mouseY);
continueGesture(mouseX, mouseY)
}
TOUCH_PAN_START -> {
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
startGesture(mouseX, mouseY)
}
TOUCH_PAN_END -> {
endGesture(mouseX, mouseY)
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
}
else -> {}
}
}
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun consumeWheelActions() { private fun consumeWheelActions() {
if (isWheelActionsPolling) { if (isWheelActionsPolling) {

View File

@ -71,17 +71,26 @@ class MainService : Service() {
@Keep @Keep
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun rustMouseInput(mask: Int, x: Int, y: Int) { fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off // turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && mask == LIFT_DOWN) { if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
Log.d(logTag,"Turn on Screen, WakeLock release") Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release() wakeLock.release()
} }
Log.d(logTag,"Turn on Screen") Log.d(logTag,"Turn on Screen")
wakeLock.acquire(5000) wakeLock.acquire(5000)
} else { } else {
InputService.ctx?.onMouseInput(mask,x,y) when (kind) {
"touch" -> {
InputService.ctx?.onTouchInput(mask, x, y)
}
"mouse" -> {
InputService.ctx?.onMouseInput(mask, x, y)
}
else -> {
}
}
} }
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="236" preserveAspectRatio="xMidYMid"><path fill="#e24329" d="m128.075 236.075 47.104-144.97H80.97z"/><path fill="#fc6d26" d="M128.075 236.074 80.97 91.104H14.956z"/><path fill="#fca326" d="M14.956 91.104.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012z"/><path fill="#e24329" d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0z"/><path fill="#fc6d26" d="m128.075 236.074 47.104-144.97h66.015z"/><path fill="#fca326" d="m241.194 91.104 14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012z"/><path fill="#e24329" d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@ -1077,7 +1077,7 @@ Color str2color(String str, [alpha = 0xFF]) {
return Color((hash & 0xFF7FFF) | (alpha << 24)); return Color((hash & 0xFF7FFF) | (alpha << 24));
} }
Color str2color2(String str, [alpha = 0xFF]) { Color str2color2(String str, {List<int> existing = const []}) {
Map<String, Color> colorMap = { Map<String, Color> colorMap = {
"red": Colors.red, "red": Colors.red,
"green": Colors.green, "green": Colors.green,
@ -1094,10 +1094,10 @@ Color str2color2(String str, [alpha = 0xFF]) {
}; };
final color = colorMap[str.toLowerCase()]; final color = colorMap[str.toLowerCase()];
if (color != null) { if (color != null) {
return color.withAlpha(alpha); return color.withAlpha(0xFF);
} }
if (str.toLowerCase() == 'yellow') { if (str.toLowerCase() == 'yellow') {
return Colors.yellow.withAlpha(alpha); return Colors.yellow.withAlpha(0xFF);
} }
var hash = 0; var hash = 0;
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
@ -1105,7 +1105,15 @@ Color str2color2(String str, [alpha = 0xFF]) {
} }
List<Color> colorList = colorMap.values.toList(); List<Color> colorList = colorMap.values.toList();
hash = hash % colorList.length; hash = hash % colorList.length;
return colorList[hash].withAlpha(alpha); var result = colorList[hash].withAlpha(0xFF);
if (existing.contains(result.value)) {
Color? notUsed =
colorList.firstWhereOrNull((e) => !existing.contains(e.value));
if (notUsed != null) {
result = notUsed;
}
}
return result;
} }
const K = 1024; const K = 1024;

View File

@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import '../../common.dart'; import '../../common.dart';
import 'dialog.dart'; import 'dialog.dart';
@ -513,7 +514,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: tags.contains(name) color: tags.contains(name)
? str2color2(name, 0xFF) ? gFFI.abModel.getTagColor(name)
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)), borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@ -528,7 +529,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: tags.contains(name) color: tags.contains(name)
? Colors.white ? Colors.white
: str2color2(name)), : gFFI.abModel.getTagColor(name)),
).marginOnly(right: radius / 2), ).marginOnly(right: radius / 2),
Expanded( Expanded(
child: Text(name, child: Text(name,
@ -568,6 +569,30 @@ class AddressBookTag extends StatelessWidget {
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
}); });
}), }),
getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel;
Color oldColor = model.getTagColor(name);
Color newColor = await showColorPickerDialog(
context,
oldColor,
pickersEnabled: {
ColorPickerType.accent: false,
ColorPickerType.wheel: true,
},
pickerTypeLabels: {
ColorPickerType.primary: translate("Primary Color"),
ColorPickerType.wheel: translate("HSV Color"),
},
actionButtons: ColorPickerActionButtons(
dialogOkButtonLabel: translate("OK"),
dialogCancelButtonLabel: translate("Cancel")),
showColorCode: true,
);
if (oldColor != newColor) {
model.setTagColor(name, newColor);
model.pushAb();
}
}),
getEntry(translate("Delete"), () { getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name); gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb(); gFFI.abModel.pushAb();

View File

@ -14,6 +14,7 @@ import './dialog.dart';
const kOpSvgList = [ const kOpSvgList = [
'github', 'github',
'gitlab',
'google', 'google',
'apple', 'apple',
'okta', 'okta',
@ -72,6 +73,11 @@ class ButtonOP extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final opLabel = {
'github': 'GitHub',
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
return Row(children: [ return Row(children: [
Container( Container(
height: height, height: height,
@ -97,8 +103,7 @@ class ButtonOP extends StatelessWidget {
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Center( child: Center(
child: Text( child: Text('${translate("Continue with")} $opLabel')),
'${translate("Continue with")} ${op.toLowerCase() == "github" ? "GitHub" : toCapitalized(op)}')),
), ),
), ),
], ],

View File

@ -63,58 +63,29 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() { Widget _buildMobile() {
final peer = super.widget.peer; final peer = super.widget.peer;
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer); peerTabModel.select(peer);
}, } else {
child: Container( if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: Row( child: _buildPeerTile(context, peer, null)),
children: [ ));
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.all(6),
child: getPlatformImage(peer.platform)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
getOnline(4, peer.online),
Text(peer.alias.isEmpty
? formatID(peer.id)
: peer.alias)
]),
Text(name)
],
).paddingOnly(left: 8.0),
),
checkBoxOrActionMoreMobile(peer),
],
),
)));
} }
Widget _buildDesktop() { Widget _buildDesktop() {
@ -161,87 +132,96 @@ class _PeerCardState extends State<_PeerCard>
} }
Widget _buildPeerTile( Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Obx( final child = Row(
() => Container( mainAxisSize: MainAxisSize.max,
foregroundDecoration: deco.value, children: [
child: Row( Container(
mainAxisSize: MainAxisSize.max, decoration: BoxDecoration(
children: [ color: str2color('${peer.id}${peer.platform}', 0x7f),
Container( borderRadius: isMobile
decoration: BoxDecoration( ? BorderRadius.circular(_tileRadius)
color: str2color('${peer.id}${peer.platform}', 0x7f), : BorderRadius.only(
borderRadius: BorderRadius.only( topLeft: Radius.circular(_tileRadius),
topLeft: Radius.circular(_tileRadius), bottomLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: 42,
child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
), ),
), ),
child: Row( alignment: Alignment.center,
children: [ width: isMobile ? 50 : 42,
Expanded( height: isMobile ? 50 : null,
child: Column( child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
children: [ .paddingAll(6),
Row(children: [
getOnline(8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty
? formatID(peer.id)
: peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(bottom: 0, top: 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
), ),
), Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(children: [
getOnline(isMobile ? 4 : 8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(top: isMobile ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isMobile ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
isMobile
? checkBoxOrActionMoreMobile(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: isMobile
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? ''
: '', : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [ child: Stack(children: [
child, deco == null
? child
: Obx(
() => Container(
foregroundDecoration: deco.value,
child: child,
),
),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Positioned(
top: 2, top: 2,
right: 10, right: isMobile ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
@ -332,7 +312,8 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList(); final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
@ -754,11 +735,12 @@ abstract class BasePeerCard extends StatelessWidget {
proc: () async { proc: () async {
bool result = gFFI.abModel.changePassword(id, ''); bool result = gFFI.abModel.changePassword(id, '');
await bind.mainForgetPassword(id: id); await bind.mainForgetPassword(id: id);
bool toast = false;
if (result) { if (result) {
bool toast = tab == PeerTabIndex.ab; toast = tab == PeerTabIndex.ab;
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast); gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
} }
showToast(translate('Successful')); if (!toast) showToast(translate('Successful'));
}, },
padding: menuPadding, padding: menuPadding,
dismissOnClicked: true, dismissOnClicked: true,

View File

@ -426,7 +426,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget selectionCount(int count) { Widget selectionCount(int count) {
return Align( return Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text('$count selected'), child: Text('$count ${translate('Selected')}'),
); );
} }

View File

@ -421,15 +421,12 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) { if (selectedTags.isEmpty) {
return true; return true;
} }
if (idents.isEmpty) {
return false;
}
for (final tag in selectedTags) { for (final tag in selectedTags) {
if (!idents.contains(tag)) { if (idents.contains(tag)) {
return false; return true;
} }
} }
return true; return false;
} }
} }

View File

@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/input_model.dart';
@ -263,9 +264,9 @@ class _RawTouchGestureDetectorRegionState
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': scale} PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
})); .toJson()));
} }
} else { } else {
// mobile // mobile
@ -283,9 +284,8 @@ class _RawTouchGestureDetectorRegionState
if (isDesktop) { if (isDesktop) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': 0} PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
}));
} else { } else {
// mobile // mobile
_scale = 1; _scale = 1;

View File

@ -5,6 +5,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0; const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
const int kMainWindowId = 0; const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
@ -38,7 +39,7 @@ const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventCloseForSeparateWindow = "close_for_separate_window"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
@ -54,6 +55,9 @@ const String kTabLabelSettingPage = "Settings";
const String kWindowPrefix = "wm_"; const String kWindowPrefix = "wm_";
const int kWindowMainId = 0; const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";

View File

@ -17,7 +17,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart'; import '../../common/widgets/login.dart';

View File

@ -35,6 +35,7 @@ class RemotePage extends StatefulWidget {
Key? key, Key? key,
required this.id, required this.id,
required this.sessionId, required this.sessionId,
required this.tabWindowId,
required this.password, required this.password,
required this.toolbarState, required this.toolbarState,
required this.tabController, required this.tabController,
@ -44,6 +45,7 @@ class RemotePage extends StatefulWidget {
final String id; final String id;
final SessionID? sessionId; final SessionID? sessionId;
final int? tabWindowId;
final String? password; final String? password;
final ToolbarState toolbarState; final ToolbarState toolbarState;
final String? switchUuid; final String? switchUuid;
@ -106,6 +108,7 @@ class _RemotePageState extends State<RemotePage>
password: widget.password, password: widget.password,
switchUuid: widget.switchUuid, switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);

View File

@ -55,6 +55,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
RemoteCountState.init(); RemoteCountState.init();
peerId = params['id']; peerId = params['id'];
final sessionId = params['session_id']; final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
if (peerId != null) { if (peerId != null) {
ConnectionTypeState.init(peerId!); ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) { tabController.onSelected = (id) {
@ -77,6 +78,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId!, id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: params['password'], password: params['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@ -98,12 +100,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
print( print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId // for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) { if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
final switchUuid = args['switch_uuid']; final switchUuid = args['switch_uuid'];
final sessionId = args['session_id']; final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
windowOnTop(windowId()); windowOnTop(windowId());
ConnectionTypeState.init(id); ConnectionTypeState.init(id);
_toolbarState.setShow( _toolbarState.setShow(
@ -118,6 +122,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: args['password'], password: args['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@ -147,12 +152,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}') .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList() .toList()
.join(';'); .join(';');
} else if (call.method == kWindowEventCloseForSeparateWindow) { } else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final peerId = call.arguments; final peerId = call.arguments;
closeSessionOnDispose[peerId] = false; try {
tabController.closeBy(peerId); final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == peerId)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (returnValue != null) {
closeSessionOnDispose[peerId] = false;
tabController.closeBy(peerId);
}
} }
_update_remote_count(); _update_remote_count();
return returnValue;
}); });
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@ -337,7 +354,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
)); ));
} }
if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {} if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {
menu.add(RemoteMenuEntry.insertLock(sessionId, padding,
dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(RemoteMenuEntry.insertCtrlAltDel(sessionId, padding,
dismissFunc: cancelFunc));
}
}
menu.addAll([ menu.addAll([
MenuEntryDivider<String>(), MenuEntryDivider<String>(),

View File

@ -771,7 +771,7 @@ class ScreenAdjustor {
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.result;
if (valueStr.isEmpty) { if (valueStr.isEmpty) {
_screen = null; _screen = null;
} else { } else {

View File

@ -28,6 +28,7 @@ class AbModel {
final pullError = "".obs; final pullError = "".obs;
final pushError = "".obs; final pushError = "".obs;
final tags = [].obs; final tags = [].obs;
final RxMap<String, int> tagColors = Map<String, int>.fromEntries([]).obs;
final peers = List<Peer>.empty(growable: true).obs; final peers = List<Peer>.empty(growable: true).obs;
final sortTags = shouldSortTags().obs; final sortTags = shouldSortTags().obs;
final retrying = false.obs; final retrying = false.obs;
@ -80,10 +81,11 @@ class AbModel {
if (resp.body.toLowerCase() == "null") { if (resp.body.toLowerCase() == "null") {
// normal reply, emtpy ab return null // normal reply, emtpy ab return null
tags.clear(); tags.clear();
tagColors.clear();
peers.clear(); peers.clear();
} else if (resp.body.isNotEmpty) { } else if (resp.body.isNotEmpty) {
Map<String, dynamic> json = Map<String, dynamic> json =
_jsonDecode(utf8.decode(resp.bodyBytes), resp.statusCode); _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) { if (json.containsKey('error')) {
throw json['error']; throw json['error'];
} else if (json.containsKey('data')) { } else if (json.containsKey('data')) {
@ -93,26 +95,7 @@ class AbModel {
} catch (e) {} } catch (e) {}
final data = jsonDecode(json['data']); final data = jsonDecode(json['data']);
if (data != null) { if (data != null) {
final oldOnlineIDs = _deserialize(data);
peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
_saveCache(); // save on success _saveCache(); // save on success
} }
} }
@ -242,10 +225,7 @@ class AbModel {
final api = "${await bind.mainGetApiServer()}/api/ab"; final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = getHttpHeaders(); var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json"; authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); final body = jsonEncode({"data": jsonEncode(_serialize())});
final body = jsonEncode({
"data": jsonEncode({"tags": tags, "peers": peersJsonData})
});
http.Response resp; http.Response resp;
// support compression // support compression
if (licensedDevices > 0 && body.length > 1024) { if (licensedDevices > 0 && body.length > 1024) {
@ -261,7 +241,7 @@ class AbModel {
ret = true; ret = true;
_saveCache(); _saveCache();
} else { } else {
Map<String, dynamic> json = _jsonDecode(resp.body, resp.statusCode); Map<String, dynamic> json = _jsonDecodeResp(resp.body, resp.statusCode);
if (json.containsKey('error')) { if (json.containsKey('error')) {
throw json['error']; throw json['error'];
} else if (resp.statusCode == 200) { } else if (resp.statusCode == 200) {
@ -318,6 +298,7 @@ class AbModel {
void deleteTag(String tag) { void deleteTag(String tag) {
gFFI.abModel.selectedTags.remove(tag); gFFI.abModel.selectedTags.remove(tag);
tags.removeWhere((element) => element == tag); tags.removeWhere((element) => element == tag);
tagColors.remove(tag);
for (var peer in peers) { for (var peer in peers) {
if (peer.tags.isEmpty) { if (peer.tags.isEmpty) {
continue; continue;
@ -353,6 +334,11 @@ class AbModel {
} }
}).toList(); }).toList();
} }
int? oldColor = tagColors[oldTag];
if (oldColor != null) {
tagColors.remove(oldTag);
tagColors.addAll({newTag: oldColor});
}
} }
void unsetSelectedTags() { void unsetSelectedTags() {
@ -368,6 +354,20 @@ class AbModel {
} }
} }
Color getTagColor(String tag) {
int? colorValue = tagColors[tag];
if (colorValue != null) {
return Color(colorValue);
}
return str2color2(tag, existing: tagColors.values.toList());
}
setTagColor(String tag, Color color) {
if (tags.contains(tag)) {
tagColors[tag] = color.value;
}
}
void merge(Peer r, Peer p) { void merge(Peer r, Peer p) {
p.hash = r.hash.isEmpty ? p.hash : r.hash; p.hash = r.hash.isEmpty ? p.hash : r.hash;
p.username = r.username.isEmpty ? p.username : r.username; p.username = r.username.isEmpty ? p.username : r.username;
@ -467,12 +467,10 @@ class AbModel {
_saveCache() { _saveCache() {
try { try {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); var m = _serialize();
final m = <String, dynamic>{ m.addAll(<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'), "access_token": bind.mainGetLocalOption(key: 'access_token'),
"peers": peersJsonData, });
"tags": tags.map((e) => e.toString()).toList(),
};
bind.mainSaveAb(json: jsonEncode(m)); bind.mainSaveAb(json: jsonEncode(m));
} catch (e) { } catch (e) {
debugPrint('ab save:$e'); debugPrint('ab save:$e');
@ -488,22 +486,13 @@ class AbModel {
final cache = await bind.mainLoadAb(); final cache = await bind.mainLoadAb();
final data = jsonDecode(cache); final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return; if (data == null || data['access_token'] != access_token) return;
tags.clear(); _deserialize(data);
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
} catch (e) { } catch (e) {
debugPrint("load ab cache: $e"); debugPrint("load ab cache: $e");
} }
} }
Map<String, dynamic> _jsonDecode(String body, int statusCode) { Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try { try {
Map<String, dynamic> json = jsonDecode(body); Map<String, dynamic> json = jsonDecode(body);
return json; return json;
@ -516,6 +505,50 @@ class AbModel {
} }
} }
Map<String, dynamic> _serialize() {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
final tagColorJsonData = jsonEncode(tagColors);
return {
"tags": tags,
"peers": peersJsonData,
"tag_colors": tagColorJsonData
};
}
_deserialize(dynamic data) {
if (data == null) return;
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
tagColors.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
if (data['tag_colors'] is String) {
Map<String, dynamic> map = jsonDecode(data['tag_colors']);
tagColors.value = Map<String, int>.from(map);
}
// add color to tag
final tagsWithoutColor =
tags.toList().where((e) => !tagColors.containsKey(e)).toList();
for (var t in tagsWithoutColor) {
tagColors[t] = str2color2(t, existing: tagColors.values.toList()).value;
}
}
reSyncToast(Future<bool> future) { reSyncToast(Future<bool> future) {
if (!shouldSyncAb()) return; if (!shouldSyncAb()) return;
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'; import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
@ -21,7 +20,6 @@ class RenderTexture {
_sessionId = sessionId; _sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async { textureRenderer.createTexture(_textureKey).then((id) async {
debugPrint("id: $id, texture_key: $_textureKey");
if (id != -1) { if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey); final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr); platformFFI.registerTexture(sessionId, ptr);

View File

@ -35,6 +35,24 @@ extension ToString on MouseButtons {
} }
} }
class PointerEventToRust {
final String kind;
final String type;
final dynamic value;
PointerEventToRust(this.kind, this.type, this.value);
Map<String, dynamic> toJson() {
return {
'k': kind,
'v': {
't': type,
'v': value,
}
};
}
}
class InputModel { class InputModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
String keyboardMode = "legacy"; String keyboardMode = "legacy";
@ -62,11 +80,11 @@ class InputModel {
int _lastButtons = 0; int _lastButtons = 0;
Offset lastMousePos = Offset.zero; Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? "";
late final SessionID sessionId; late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
@ -223,14 +241,8 @@ class InputModel {
command: command); command: command);
} }
Map<String, dynamic> getEvent(PointerEvent evt, String type) { Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {}; final Map<String, dynamic> out = {};
out['x'] = evt.position.dx;
out['y'] = evt.position.dy;
if (alt) out['alt'] = 'true';
if (shift) out['shift'] = 'true';
if (ctrl) out['ctrl'] = 'true';
if (command) out['command'] = 'true';
// Check update event type and set buttons to be sent. // Check update event type and set buttons to be sent.
int buttons = _lastButtons; int buttons = _lastButtons;
@ -260,7 +272,6 @@ class InputModel {
out['buttons'] = buttons; out['buttons'] = buttons;
out['type'] = type; out['type'] = type;
return out; return out;
} }
@ -292,7 +303,7 @@ class InputModel {
} }
/// Modify the given modifier map [evt] based on current modifier key status. /// Modify the given modifier map [evt] based on current modifier key status.
Map<String, String> modify(Map<String, String> evt) { Map<String, dynamic> modify(Map<String, dynamic> evt) {
if (ctrl) evt['ctrl'] = 'true'; if (ctrl) evt['ctrl'] = 'true';
if (shift) evt['shift'] = 'true'; if (shift) evt['shift'] = 'true';
if (alt) evt['alt'] = 'true'; if (alt) evt['alt'] = 'true';
@ -334,27 +345,33 @@ class InputModel {
isPhysicalMouse.value = true; isPhysicalMouse.value = true;
} }
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove)); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
void onPointerPanZoomStart(PointerPanZoomStartEvent e) { void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
_lastScale = 1.0; _lastScale = 1.0;
_stopFling = true; _stopFling = true;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position);
}
} }
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
final scale = ((e.scale - _lastScale) * 1000).toInt(); if (peerPlatform != kPeerPlatformAndroid) {
_lastScale = e.scale; final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
if (scale != 0) { if (scale != 0) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': scale} PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
})); .toJson()));
return; return;
}
} }
final delta = e.panDelta; final delta = e.panDelta;
@ -362,7 +379,7 @@ class InputModel {
var x = delta.dx.toInt(); var x = delta.dx.toInt();
var y = delta.dy.toInt(); var y = delta.dy.toInt();
if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) { if (peerPlatform == kPeerPlatformLinux) {
_trackpadScrollUnsent += (delta * _trackpadSpeed); _trackpadScrollUnsent += (delta * _trackpadSpeed);
x = _trackpadScrollUnsent.dx.truncate(); x = _trackpadScrollUnsent.dx.truncate();
y = _trackpadScrollUnsent.dy.truncate(); y = _trackpadScrollUnsent.dy.truncate();
@ -378,9 +395,13 @@ class InputModel {
} }
} }
if (x != 0 || y != 0) { if (x != 0 || y != 0) {
bind.sessionSendMouse( if (peerPlatform == kPeerPlatformAndroid) {
sessionId: sessionId, handlePointerEvent('touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); } else {
bind.sessionSendMouse(
sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
}
} }
} }
@ -436,11 +457,15 @@ class InputModel {
} }
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_end', e.position);
return;
}
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode({ msg: json.encode(
'touch': {'scale': 0} PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
}));
waitLastFlingDone(); waitLastFlingDone();
_stopFling = false; _stopFling = false;
@ -465,21 +490,21 @@ class InputModel {
} }
} }
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventDown)); handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
} }
} }
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventUp)); handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
} }
} }
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove)); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
@ -504,19 +529,16 @@ class InputModel {
} }
void refreshMousePos() => handleMouse({ void refreshMousePos() => handleMouse({
'x': lastMousePos.dx,
'y': lastMousePos.dy,
'buttons': 0, 'buttons': 0,
'type': _kMouseEventMove, 'type': _kMouseEventMove,
}); }, lastMousePos);
void tryMoveEdgeOnExit(Offset pos) => handleMouse( void tryMoveEdgeOnExit(Offset pos) => handleMouse(
{ {
'x': pos.dx,
'y': pos.dy,
'buttons': 0, 'buttons': 0,
'type': _kMouseEventMove, 'type': _kMouseEventMove,
}, },
pos,
onExit: true, onExit: true,
); );
@ -550,17 +572,49 @@ class InputModel {
return Offset(x, y); return Offset(x, y);
} }
void handleMouse( void handlePointerEvent(String kind, String type, Offset offset) {
Map<String, dynamic> evt, { double x = offset.dx;
bool onExit = false, double y = offset.dy;
}) { if (_checkPeerControlProtected(x, y)) {
double x = evt['x']; return;
double y = max(0.0, evt['y']); }
final cursorModel = parent.target!.cursorModel; // Only touch events are handled for now. So we can just ignore buttons.
// to-do: handle mouse events
late final dynamic evtValue;
if (type == 'pan_update') {
evtValue = {
'x': x.toInt(),
'y': y.toInt(),
};
} else {
final isMoveTypes = ['pan_start', 'pan_end'];
final pos = handlePointerDevicePos(
kPointerEventKindTouch,
x,
y,
isMoveTypes.contains(type),
type,
);
if (pos == null) {
return;
}
evtValue = {
'x': pos.x,
'y': pos.y,
};
}
final evt = PointerEventToRust(kind, type, evtValue).toJson();
bind.sessionSendPointer(
sessionId: sessionId, msg: json.encode(modify(evt)));
}
bool _checkPeerControlProtected(double x, double y) {
final cursorModel = parent.target!.cursorModel;
if (cursorModel.isPeerControlProtected) { if (cursorModel.isPeerControlProtected) {
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return true;
} }
if (!cursorModel.gotMouseControl) { if (!cursorModel.gotMouseControl) {
@ -571,10 +625,23 @@ class InputModel {
cursorModel.gotMouseControl = true; cursorModel.gotMouseControl = true;
} else { } else {
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return true;
} }
} }
lastMousePos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return false;
}
void handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
}) {
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
}
var type = ''; var type = '';
var isMove = false; var isMove = false;
@ -592,17 +659,58 @@ class InputModel {
return; return;
} }
evt['type'] = type; evt['type'] = type;
final pos = handlePointerDevicePos(
kPointerEventKindMouse,
x,
y,
isMove,
type,
onExit: onExit,
buttons: evt['buttons'],
);
if (pos == null) {
return;
}
if (type != '') {
evt['x'] = '0';
evt['y'] = '0';
} else {
evt['x'] = '${pos.x}';
evt['y'] = '${pos.y}';
}
Map<int, String> mapButtons = {
kPrimaryMouseButton: 'left',
kSecondaryMouseButton: 'right',
kMiddleMouseButton: 'wheel',
kBackMouseButton: 'back',
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
}
Point? handlePointerDevicePos(
String kind,
double x,
double y,
bool isMove,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
y -= CanvasModel.topToEdge; y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge; x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel; final canvasModel = parent.target!.canvasModel;
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final ffiModel = parent.target!.ffiModel; final ffiModel = parent.target!.ffiModel;
if (isMove) { if (isMove) {
canvasModel.moveDesktopMouse(x, y); canvasModel.moveDesktopMouse(x, y);
} }
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final d = ffiModel.display; final d = ffiModel.display;
final imageWidth = d.width * canvasModel.scale; final imageWidth = d.width * canvasModel.scale;
final imageHeight = d.height * canvasModel.scale; final imageHeight = d.height * canvasModel.scale;
@ -650,7 +758,7 @@ class InputModel {
} catch (e) { } catch (e) {
debugPrintStack( debugPrintStack(
label: 'canvasModel.scale value ${canvasModel.scale}, $e'); label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return; return null;
} }
int minX = d.x.toInt(); int minX = d.x.toInt();
@ -659,40 +767,16 @@ class InputModel {
int maxY = (d.y + d.height).toInt() - 1; int maxY = (d.y + d.height).toInt() - 1;
evtX = trySetNearestRange(evtX, minX, maxX, 5); evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5); evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { if (kind == kPointerEventKindMouse) {
// If left mouse up, no early return. if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { // If left mouse up, no early return.
return; if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
return null;
}
} }
} }
if (type != '') { return Point(evtX, evtY);
evtX = 0;
evtY = 0;
}
evt['x'] = '$evtX';
evt['y'] = '$evtY';
var buttons = '';
switch (evt['buttons']) {
case kPrimaryMouseButton:
buttons = 'left';
break;
case kSecondaryMouseButton:
buttons = 'right';
break;
case kMiddleMouseButton:
buttons = 'wheel';
break;
case kBackMouseButton:
buttons = 'back';
break;
case kForwardMouseButton:
buttons = 'forward';
break;
}
evt['buttons'] = buttons;
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(evt));
} }
/// Web only /// Web only

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@ -41,7 +42,50 @@ final _waitForImageDialogShow = <UuidValue, bool>{};
final _waitForFirstImage = <UuidValue, bool>{}; final _waitForFirstImage = <UuidValue, bool>{};
final _constSessionId = Uuid().v4obj(); final _constSessionId = Uuid().v4obj();
class CachedPeerData {
Map<String, dynamic> updatePrivacyMode = {};
Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {};
bool secure = false;
bool direct = false;
CachedPeerData();
@override
String toString() {
return jsonEncode({
'updatePrivacyMode': updatePrivacyMode,
'peerInfo': peerInfo,
'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId,
'secure': secure,
'direct': direct,
});
}
static CachedPeerData? fromString(String s) {
try {
final map = jsonDecode(s);
final data = CachedPeerData();
data.updatePrivacyMode = map['updatePrivacyMode'];
data.peerInfo = map['peerInfo'];
for (final cursorData in map['cursorDataList']) {
data.cursorDataList.add(cursorData);
}
data.lastCursorId = map['lastCursorId'];
data.secure = map['secure'];
data.direct = map['direct'];
return data;
} catch (e) {
debugPrint('Failed to parse CachedPeerData: $e');
return null;
}
}
}
class FfiModel with ChangeNotifier { class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo(); PeerInfo _pi = PeerInfo();
Display _display = Display(); Display _display = Display();
@ -117,6 +161,8 @@ class FfiModel with ChangeNotifier {
} }
setConnectionType(String peerId, bool secure, bool direct) { setConnectionType(String peerId, bool secure, bool direct) {
cachedPeerData.secure = secure;
cachedPeerData.direct = direct;
_secure = secure; _secure = secure;
_direct = direct; _direct = direct;
try { try {
@ -143,6 +189,22 @@ class FfiModel with ChangeNotifier {
_permissions.clear(); _permissions.clear();
} }
handleCachedPeerData(CachedPeerData data, String peerId) async {
handleMsgBox({
'type': 'success',
'title': 'Successful',
'text': 'Connected, waiting for image...',
'link': '',
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
handlePeerInfo(data.peerInfo, peerId);
for (var element in data.cursorDataList) {
handleCursorData(element);
}
handleCursorId(data.lastCursorId);
}
// todo: why called by two position // todo: why called by two position
StreamEventHandler startEventListener(SessionID sessionId, String peerId) { StreamEventHandler startEventListener(SessionID sessionId, String peerId) {
return (evt) async { return (evt) async {
@ -159,9 +221,9 @@ class FfiModel with ChangeNotifier {
} else if (name == 'switch_display') { } else if (name == 'switch_display') {
handleSwitchDisplay(evt, sessionId, peerId); handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') { } else if (name == 'cursor_data') {
await parent.target?.cursorModel.updateCursorData(evt); await handleCursorData(evt);
} else if (name == 'cursor_id') { } else if (name == 'cursor_id') {
await parent.target?.cursorModel.updateCursorId(evt); await handleCursorId(evt);
} else if (name == 'cursor_position') { } else if (name == 'cursor_position') {
await parent.target?.cursorModel.updateCursorPosition(evt, peerId); await parent.target?.cursorModel.updateCursorPosition(evt, peerId);
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
@ -464,6 +526,8 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt]. /// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId) async { handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
cachedPeerData.peerInfo = evt;
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers(); bind.mainLoadRecentPeers();
@ -579,9 +643,20 @@ class FfiModel with ChangeNotifier {
return d; return d;
} }
handleCursorId(Map<String, dynamic> evt) async {
cachedPeerData.lastCursorId = evt;
await parent.target?.cursorModel.updateCursorId(evt);
}
handleCursorData(Map<String, dynamic> evt) async {
cachedPeerData.cursorDataList.add(evt);
await parent.target?.cursorModel.updateCursorData(evt);
}
/// Handle the peer info synchronization event based on [evt]. /// Handle the peer info synchronization event based on [evt].
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async { handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
if (evt['displays'] != null) { if (evt['displays'] != null) {
cachedPeerData.peerInfo['displays'] = evt['displays'];
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) {
@ -1596,7 +1671,6 @@ class FFI {
/// dialogManager use late to ensure init after main page binding [globalKey] /// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager(); late final dialogManager = OverlayDialogManager();
late final bool isSessionAdded;
late final SessionID sessionId; late final SessionID sessionId;
late final ImageModel imageModel; // session late final ImageModel imageModel; // session
late final FfiModel ffiModel; // session late final FfiModel ffiModel; // session
@ -1615,7 +1689,6 @@ class FFI {
late final ElevationModel elevationModel; // session late final ElevationModel elevationModel; // session
FFI(SessionID? sId) { FFI(SessionID? sId) {
isSessionAdded = sId != null;
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
imageModel = ImageModel(WeakReference(this)); imageModel = ImageModel(WeakReference(this));
ffiModel = FfiModel(WeakReference(this)); ffiModel = FfiModel(WeakReference(this));
@ -1641,7 +1714,8 @@ class FFI {
bool isRdp = false, bool isRdp = false,
String? switchUuid, String? switchUuid,
String? password, String? password,
bool? forceRelay}) { bool? forceRelay,
int? tabWindowId}) {
closed = false; closed = false;
auditNote = ''; auditNote = '';
assert(!(isFileTransfer && isPortForward), 'more than one connect type'); assert(!(isFileTransfer && isPortForward), 'more than one connect type');
@ -1656,7 +1730,9 @@ class FFI {
imageModel.id = id; imageModel.id = id;
cursorModel.id = id; cursorModel.id = id;
} }
if (!isSessionAdded) { // If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
if (tabWindowId == null) {
// ignore: unused_local_variable // ignore: unused_local_variable
final addRes = bind.sessionAddSync( final addRes = bind.sessionAddSync(
sessionId: sessionId, sessionId: sessionId,
@ -1677,8 +1753,25 @@ class FFI {
// Preserved for the rgba data. // Preserved for the rgba data.
stream.listen((message) { stream.listen((message) {
if (closed) return; if (closed) return;
if (isSessionAdded && !isToNewWindowNotified.value) { if (tabWindowId != null && !isToNewWindowNotified.value) {
bind.sessionReadyToNewWindow(sessionId: sessionId); // Session is read to be moved to a new window.
// Get the cached data and handle the cached data.
Future.delayed(Duration.zero, () async {
final cachedData = await DesktopMultiWindow.invokeMethod(
tabWindowId, kWindowEventGetCachedSessionData, id);
if (cachedData == null) {
// unreachable
debugPrint('Unreachable, the cached data is empty.');
return;
}
final data = CachedPeerData.fromString(cachedData);
if (data == null) {
debugPrint('Unreachable, the cached data cannot be decoded.');
return;
}
ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId);
});
isToNewWindowNotified.value = true; isToNewWindowNotified.value = true;
} }
() async { () async {

View File

@ -28,6 +28,13 @@ extension Index on int {
} }
} }
class MultiWindowCallResult {
int windowId;
dynamic result;
MultiWindowCallResult(this.windowId, this.result);
}
/// Window Manager /// Window Manager
/// mainly use it in `Main Window` /// mainly use it in `Main Window`
/// use it in sub window is not recommended /// use it in sub window is not recommended
@ -47,6 +54,7 @@ class RustDeskMultiWindowManager {
var params = { var params = {
'type': WindowType.RemoteDesktop.index, 'type': WindowType.RemoteDesktop.index,
'id': peerId, 'id': peerId,
'tab_window_id': windowId,
'session_id': sessionId, 'session_id': sessionId,
}; };
await _newSession( await _newSession(
@ -57,17 +65,15 @@ class RustDeskMultiWindowManager {
_remoteDesktopWindows, _remoteDesktopWindows,
jsonEncode(params), jsonEncode(params),
); );
await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventCloseForSeparateWindow, peerId);
} }
newSessionWindow( Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async { WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg); final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
windowController windowController
..setFrame(const Offset(0, 0) & ..setFrame(
Size(1280 + windowController.windowId * 20, const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
720 + windowController.windowId * 20))
..center() ..center()
..setTitle(getWindowNameWithId( ..setTitle(getWindowNameWithId(
remoteId, remoteId,
@ -76,11 +82,12 @@ class RustDeskMultiWindowManager {
if (Platform.isMacOS) { if (Platform.isMacOS) {
Future.microtask(() => windowController.show()); Future.microtask(() => windowController.show());
} }
registerActiveWindow(windowController.windowId); registerActiveWindow(windowId);
windows.add(windowController.windowId); windows.add(windowId);
return windowId;
} }
_newSession( Future<MultiWindowCallResult> _newSession(
bool openInTabs, bool openInTabs,
WindowType type, WindowType type,
String methodName, String methodName,
@ -90,9 +97,10 @@ class RustDeskMultiWindowManager {
) async { ) async {
if (openInTabs) { if (openInTabs) {
if (windows.isEmpty) { if (windows.isEmpty) {
await newSessionWindow(type, remoteId, msg, windows); final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
} else { } else {
call(type, methodName, msg); return call(type, methodName, msg);
} }
} else { } else {
if (_inactiveWindows.isNotEmpty) { if (_inactiveWindows.isNotEmpty) {
@ -103,15 +111,16 @@ class RustDeskMultiWindowManager {
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg); await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
WindowController.fromWindowId(windowId).show(); WindowController.fromWindowId(windowId).show();
registerActiveWindow(windowId); registerActiveWindow(windowId);
return; return MultiWindowCallResult(windowId, null);
} }
} }
} }
await newSessionWindow(type, remoteId, msg, windows); final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
} }
} }
Future<dynamic> newSession( Future<MultiWindowCallResult> newSession(
WindowType type, WindowType type,
String methodName, String methodName,
String remoteId, String remoteId,
@ -143,15 +152,15 @@ class RustDeskMultiWindowManager {
for (final windowId in windows) { for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod( if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) { windowId, kWindowEventActiveSession, remoteId)) {
return; return MultiWindowCallResult(windowId, null);
} }
} }
} }
await _newSession(openInTabs, type, methodName, remoteId, windows, msg); return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
} }
Future<dynamic> newRemoteDesktop( Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, { String remoteId, {
String? password, String? password,
String? switchUuid, String? switchUuid,
@ -168,7 +177,7 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> newFileTransfer(String remoteId, Future<MultiWindowCallResult> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async { {String? password, bool? forceRelay}) async {
return await newSession( return await newSession(
WindowType.FileTransfer, WindowType.FileTransfer,
@ -180,7 +189,7 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> newPortForward(String remoteId, bool isRDP, Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
{String? password, bool? forceRelay}) async { {String? password, bool? forceRelay}) async {
return await newSession( return await newSession(
WindowType.PortForward, WindowType.PortForward,
@ -193,18 +202,22 @@ class RustDeskMultiWindowManager {
); );
} }
Future<dynamic> call(WindowType type, String methodName, dynamic args) async { Future<MultiWindowCallResult> call(
WindowType type, String methodName, dynamic args) async {
final wnds = _findWindowsByType(type); final wnds = _findWindowsByType(type);
if (wnds.isEmpty) { if (wnds.isEmpty) {
return; return MultiWindowCallResult(kInvalidWindowId, null);
} }
for (final windowId in wnds) { for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) { if (_activeWindows.contains(windowId)) {
return await DesktopMultiWindow.invokeMethod( final res =
windowId, methodName, args); await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
return MultiWindowCallResult(windowId, res);
} }
} }
return await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args); final res =
await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
return MultiWindowCallResult(wnds[0], res);
} }
List<int> _findWindowsByType(WindowType type) { List<int> _findWindowsByType(WindowType type) {

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.2.2 version: 1.2.3
environment: environment:
sdk: ">=2.17.0" sdk: ">=2.17.0"
@ -97,6 +97,7 @@ dependencies:
dropdown_button2: ^2.0.0 dropdown_button2: ^2.0.0
uuid: ^3.0.7 uuid: ^3.0.7
auto_size_text_field: ^2.2.1 auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4

View File

@ -118,9 +118,29 @@ message TouchScaleUpdate {
int32 scale = 1; int32 scale = 1;
} }
message TouchPanStart {
int32 x = 1;
int32 y = 2;
}
message TouchPanUpdate {
// The delta x position relative to the previous position.
int32 x = 1;
// The delta y position relative to the previous position.
int32 y = 2;
}
message TouchPanEnd {
int32 x = 1;
int32 y = 2;
}
message TouchEvent { message TouchEvent {
oneof union { oneof union {
TouchScaleUpdate scale_update = 1; TouchScaleUpdate scale_update = 1;
TouchPanStart pan_start = 2;
TouchPanUpdate pan_update = 3;
TouchPanEnd pan_end = 4;
} }
} }

View File

@ -1525,6 +1525,12 @@ pub struct Ab {
pub peers: Vec<AbPeer>, pub peers: Vec<AbPeer>,
#[serde(default, deserialize_with = "deserialize_vec_string")] #[serde(default, deserialize_with = "deserialize_vec_string")]
pub tags: Vec<String>, pub tags: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub tag_colors: String,
} }
impl Ab { impl Ab {

View File

@ -154,17 +154,18 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
} }
} }
pub fn call_main_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> { pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = ( if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(), JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(), MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) { ) {
let mut env = jvm.attach_current_thread_as_daemon()?; let mut env = jvm.attach_current_thread_as_daemon()?;
let kind = env.new_string(kind)?;
env.call_method( env.call_method(
ctx, ctx,
"rustMouseInput", "rustPointerInput",
"(III)V", "(Ljava/lang/String;III)V",
&[JValue::Int(mask), JValue::Int(x), JValue::Int(y)], &[JValue::Object(&JObject::from(kind)), JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
)?; )?;
return Ok(()); return Ok(());
} else { } else {

View File

@ -1,5 +1,5 @@
pkgname=rustdesk pkgname=rustdesk
pkgver=1.2.2 pkgver=1.2.3
pkgrel=0 pkgrel=0
epoch= epoch=
pkgdesc="" pkgdesc=""

View File

@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.2.2 Version: 1.2.3
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.2.2 Version: 1.2.3
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.2.2 Version: 1.2.3
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@ -2387,7 +2387,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn send(&self, data: Data); fn send(&self, data: Data);
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str);
fn handle_login_error(&mut self, err: &str) -> bool; fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo, is_cached_pi: bool); fn handle_peer_info(&mut self, pi: PeerInfo);
fn on_error(&self, err: &str) { fn on_error(&self, err: &str) {
self.msgbox("error", "Error", err, ""); self.msgbox("error", "Error", err, "");
} }

View File

@ -125,18 +125,7 @@ impl<T: InvokeUiSession> Remote<T> {
.await .await
{ {
Ok((mut peer, direct, pk)) => { Ok((mut peer, direct, pk)) => {
let is_secured = peer.is_secured(); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.handler
.cache_flutter
.write()
.unwrap()
.is_secured_direct
.replace((is_secured, direct));
}
self.handler.set_connection_type(is_secured, direct); // flutter -> connection_ready
self.handler.update_direct(Some(direct)); self.handler.update_direct(Some(direct));
if conn_type == ConnType::DEFAULT_CONN { if conn_type == ConnType::DEFAULT_CONN {
self.handler self.handler
@ -1021,12 +1010,7 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
Some(login_response::Union::PeerInfo(pi)) => { Some(login_response::Union::PeerInfo(pi)) => {
#[cfg(feature = "flutter")] self.handler.handle_peer_info(pi);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.handler.cache_flutter.write().unwrap().pi = pi.clone();
}
self.handler.handle_peer_info(pi, false);
#[cfg(not(feature = "flutter"))] #[cfg(not(feature = "flutter"))]
self.check_clipboard_file_context(); self.check_clipboard_file_context();
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
@ -1073,22 +1057,9 @@ impl<T: InvokeUiSession> Remote<T> {
_ => {} _ => {}
}, },
Some(message::Union::CursorData(cd)) => { Some(message::Union::CursorData(cd)) => {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let mut lock = self.handler.cache_flutter.write().unwrap();
if !lock.cursor_data.contains_key(&cd.id) {
lock.cursor_data.insert(cd.id, cd.clone());
}
}
self.handler.set_cursor_data(cd); self.handler.set_cursor_data(cd);
} }
Some(message::Union::CursorId(id)) => { Some(message::Union::CursorId(id)) => {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.handler.cache_flutter.write().unwrap().cursor_id = id;
}
self.handler.set_cursor_id(id.to_string()); self.handler.set_cursor_id(id.to_string());
} }
Some(message::Union::CursorPosition(cp)) => { Some(message::Union::CursorPosition(cp)) => {
@ -1305,16 +1276,6 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
Some(misc::Union::SwitchDisplay(s)) => { Some(misc::Union::SwitchDisplay(s)) => {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.handler
.cache_flutter
.write()
.unwrap()
.sp
.replace(s.clone());
}
self.handler.handle_peer_switch_display(&s); self.handler.handle_peer_switch_display(&s);
self.video_sender.send(MediaData::Reset).ok(); self.video_sender.send(MediaData::Reset).ok();
if s.width > 0 && s.height > 0 { if s.width > 0 && s.height > 0 {
@ -1506,12 +1467,6 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
Some(message::Union::PeerInfo(pi)) => { Some(message::Union::PeerInfo(pi)) => {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.handler.cache_flutter.write().unwrap().pi.displays =
pi.displays.clone();
}
self.handler.set_displays(&pi.displays); self.handler.set_displays(&pi.displays);
} }
_ => {} _ => {}

View File

@ -831,30 +831,19 @@ pub fn check_software_update() {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn check_software_update_() -> hbb_common::ResultType<()> { async fn check_software_update_() -> hbb_common::ResultType<()> {
sleep(3.).await; let url = "https://github.com/rustdesk/rustdesk/releases/latest";
let latest_release_response = reqwest::get(url).await?;
let latest_release_version = latest_release_response
.url()
.path()
.rsplit('/')
.next()
.unwrap();
let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT); let response_url = latest_release_response.url().to_string();
let (mut socket, rendezvous_server) =
socket_client::new_udp_for(&rendezvous_server, CONNECT_TIMEOUT).await?;
let mut msg_out = RendezvousMessage::new(); if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
msg_out.set_software_update(SoftwareUpdate { *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
url: crate::VERSION.to_owned(),
..Default::default()
});
socket.send(&msg_out, rendezvous_server).await?;
use hbb_common::protobuf::Message;
for _ in 0..2 {
if let Some(Ok((bytes, _))) = socket.next_timeout(READ_TIMEOUT).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
if let Some(rendezvous_message::Union::SoftwareUpdate(su)) = msg_in.union {
let version = hbb_common::get_version_from_url(&su.url);
if get_version_number(&version) > get_version_number(crate::VERSION) {
*SOFTWARE_UPDATE_URL.lock().unwrap() = su.url;
}
}
}
}
} }
Ok(()) Ok(())
} }

View File

@ -36,9 +36,11 @@ pub(crate) const APP_TYPE_CM: &str = "cm";
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
pub(crate) const APP_TYPE_CM: &str = "main"; pub(crate) const APP_TYPE_CM: &str = "main";
pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; // Do not remove the following constants.
pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; // Uncomment them when they are used.
pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; // pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
// pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
// pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward";
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub(crate) static ref CUR_SESSION_ID: RwLock<SessionID> = Default::default(); pub(crate) static ref CUR_SESSION_ID: RwLock<SessionID> = Default::default();
@ -1130,6 +1132,85 @@ pub fn stop_global_event_stream(app_type: String) {
let _ = GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); let _ = GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type);
} }
#[inline]
fn session_send_touch_scale(
session_id: SessionID,
v: &serde_json::Value,
alt: bool,
ctrl: bool,
shift: bool,
command: bool,
) {
match v.get("v").and_then(|s| s.as_i64()) {
Some(scale) => {
if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
session.send_touch_scale(scale as _, alt, ctrl, shift, command);
}
}
None => {}
}
}
#[inline]
fn session_send_touch_pan(
session_id: SessionID,
v: &serde_json::Value,
pan_event: &str,
alt: bool,
ctrl: bool,
shift: bool,
command: bool,
) {
match v.get("v") {
Some(v) => match (
v.get("x").and_then(|x| x.as_i64()),
v.get("y").and_then(|y| y.as_i64()),
) {
(Some(x), Some(y)) => {
if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
session
.send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command);
}
}
_ => {}
},
_ => {}
}
}
fn session_send_touch_event(
session_id: SessionID,
v: &serde_json::Value,
alt: bool,
ctrl: bool,
shift: bool,
command: bool,
) {
match v.get("t").and_then(|t| t.as_str()) {
Some("scale") => session_send_touch_scale(session_id, v, alt, ctrl, shift, command),
Some(pan_event) => {
session_send_touch_pan(session_id, v, pan_event, alt, ctrl, shift, command)
}
_ => {}
}
}
pub fn session_send_pointer(session_id: SessionID, msg: String) {
if let Ok(m) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&msg) {
let alt = m.get("alt").is_some();
let ctrl = m.get("ctrl").is_some();
let shift = m.get("shift").is_some();
let command = m.get("command").is_some();
match (m.get("k"), m.get("v")) {
(Some(k), Some(v)) => match k.as_str() {
Some("touch") => session_send_touch_event(session_id, v, alt, ctrl, shift, command),
_ => {}
},
_ => {}
}
}
}
#[no_mangle] #[no_mangle]
unsafe extern "C" fn get_rgba() {} unsafe extern "C" fn get_rgba() {}

View File

@ -597,14 +597,6 @@ pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32
} }
} }
pub fn session_ready_to_new_window(session_id: SessionID) {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(session) = SESSIONS.write().unwrap().get_mut(&session_id) {
session.restore_flutter_cache();
session.refresh_video();
}
}
pub fn session_set_size(_session_id: SessionID, _width: usize, _height: usize) { pub fn session_set_size(_session_id: SessionID, _width: usize, _height: usize) {
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
if let Some(session) = SESSIONS.write().unwrap().get_mut(&_session_id) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&_session_id) {
@ -1179,21 +1171,7 @@ pub fn main_load_ab() -> String {
} }
pub fn session_send_pointer(session_id: SessionID, msg: String) { pub fn session_send_pointer(session_id: SessionID, msg: String) {
if let Ok(m) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&msg) { super::flutter::session_send_pointer(session_id, msg);
let alt = m.get("alt").is_some();
let ctrl = m.get("ctrl").is_some();
let shift = m.get("shift").is_some();
let command = m.get("command").is_some();
if let Some(touch_event) = m.get("touch") {
if let Some(scale) = touch_event.get("scale") {
if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
if let Some(scale) = scale.as_i64() {
session.send_touch_scale(scale as _, alt, ctrl, shift, command);
}
}
}
}
}
} }
pub fn session_send_mouse(session_id: SessionID, msg: String) { pub fn session_send_mouse(session_id: SessionID, msg: String) {

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "未成功获取地址簿"), ("pull_ab_failed_tip", "未成功获取地址簿"),
("push_ab_failed_tip", "未成功上传地址簿"), ("push_ab_failed_tip", "未成功上传地址簿"),
("synced_peer_readded_tip", "最近会话中存在的设备将会被重新同步到地址簿。"), ("synced_peer_readded_tip", "最近会话中存在的设备将会被重新同步到地址簿。"),
("Change Color", "更改颜色"),
("Primary Color", "基本色"),
("HSV Color", "HSV 色"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "Nepodařilo se obnovit adresář"), ("pull_ab_failed_tip", "Nepodařilo se obnovit adresář"),
("push_ab_failed_tip", "Nepodařilo se synchronizovat adresář se serverem"), ("push_ab_failed_tip", "Nepodařilo se synchronizovat adresář se serverem"),
("synced_peer_readded_tip", "Zařízení, která byla přítomna v posledních relacích, budou synchronizována zpět do adresáře."), ("synced_peer_readded_tip", "Zařízení, která byla přítomna v posledních relacích, budou synchronizována zpět do adresáře."),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "Aktualisierung des Adressbuchs fehlgeschlagen"), ("pull_ab_failed_tip", "Aktualisierung des Adressbuchs fehlgeschlagen"),
("push_ab_failed_tip", "Synchronisierung des Adressbuchs mit dem Server fehlgeschlagen"), ("push_ab_failed_tip", "Synchronisierung des Adressbuchs mit dem Server fehlgeschlagen"),
("synced_peer_readded_tip", "Die Geräte, die in den letzten Sitzungen vorhanden waren, werden erneut zum Adressbuch hinzugefügt."), ("synced_peer_readded_tip", "Die Geräte, die in den letzten Sitzungen vorhanden waren, werden erneut zum Adressbuch hinzugefügt."),
("Change Color", "Farbe ändern"),
("Primary Color", "Primärfarbe"),
("HSV Color", "HSV-Farbe"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "No se ha podido refrescar el directorio"), ("pull_ab_failed_tip", "No se ha podido refrescar el directorio"),
("push_ab_failed_tip", "No se ha podido sincronizar el directorio con el servidor"), ("push_ab_failed_tip", "No se ha podido sincronizar el directorio con el servidor"),
("synced_peer_readded_tip", "Los dispositivos presentes en sesiones recientes se sincronizarán con el directorio."), ("synced_peer_readded_tip", "Los dispositivos presentes en sesiones recientes se sincronizarán con el directorio."),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].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 la machine distante"), ("Reset by the peer", "La connexion a été fermée par l'appareil distant"),
("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"),
@ -92,8 +92,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Refresh File", "Rafraîchir le contenu"), ("Refresh File", "Rafraîchir le contenu"),
("Local", "Local"), ("Local", "Local"),
("Remote", "Distant"), ("Remote", "Distant"),
("Remote Computer", "Ordinateur distant"), ("Remote Computer", "Appareil distant"),
("Local Computer", "Ordinateur local"), ("Local Computer", "Appareil local"),
("Confirm Delete", "Confirmer la suppression"), ("Confirm Delete", "Confirmer la suppression"),
("Delete", "Supprimer"), ("Delete", "Supprimer"),
("Properties", "Propriétés"), ("Properties", "Propriétés"),
@ -129,9 +129,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show remote cursor", "Afficher le curseur distant"), ("Show remote cursor", "Afficher le curseur distant"),
("Show quality monitor", "Afficher le moniteur de qualité"), ("Show quality monitor", "Afficher le moniteur de qualité"),
("Disable clipboard", "Désactiver le presse-papier"), ("Disable clipboard", "Désactiver le presse-papier"),
("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"),
("Insert", "Envoyer"), ("Insert", "Envoyer"),
("Insert Lock", "Verrouiller l'ordinateur distant"), ("Insert Lock", "Verrouiller l'appareil distant"),
("Refresh", "Rafraîchir l'écran"), ("Refresh", "Rafraîchir l'écran"),
("ID does not exist", "L'ID n'existe pas"), ("ID does not exist", "L'ID n'existe pas"),
("Failed to connect to rendezvous server", "Échec de la connexion au serveur rendezvous"), ("Failed to connect to rendezvous server", "Échec de la connexion au serveur rendezvous"),
@ -188,7 +188,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relayed and encrypted connection", "Connexion relais chiffrée"), ("Relayed and encrypted connection", "Connexion relais chiffrée"),
("Direct and unencrypted connection", "Connexion directe non chiffrée"), ("Direct and unencrypted connection", "Connexion directe non chiffrée"),
("Relayed and unencrypted connection", "Connexion relais non chiffrée"), ("Relayed and unencrypted connection", "Connexion relais non chiffrée"),
("Enter Remote ID", "Entrer l'ID de l'appareil à distance"), ("Enter Remote ID", "Entrer l'ID de l'appareil distant"),
("Enter your password", "Entrer votre mot de passe"), ("Enter your password", "Entrer votre mot de passe"),
("Logging in...", "En cours de connexion ..."), ("Logging in...", "En cours de connexion ..."),
("Enable RDP session sharing", "Activer le partage de session RDP"), ("Enable RDP session sharing", "Activer le partage de session RDP"),
@ -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 la machine distante"), ("Closed manually by the peer", "Fermé manuellement par l'appareil distant"),
("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"),
@ -223,12 +223,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Verification code", "Code de vérification"), ("Verification code", "Code de vérification"),
("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."), ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."),
("Logout", "Déconnexion"), ("Logout", "Déconnexion"),
("Tags", "Étiqueter"), ("Tags", "Étiquettes"),
("Search ID", "Rechercher un ID"), ("Search ID", "Rechercher un ID"),
("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"),
("Add ID", "Ajouter un ID"), ("Add ID", "Ajouter un ID"),
("Add Tag", "Ajouter une balise"), ("Add Tag", "Ajouter une balise"),
("Unselect all tags", "Désélectionner toutes les balises"), ("Unselect all tags", "Désélectionner toutes les étiquettes"),
("Network error", "Erreur réseau"), ("Network error", "Erreur réseau"),
("Username missed", "Nom d'utilisateur manquant"), ("Username missed", "Nom d'utilisateur manquant"),
("Password missed", "Mot de passe manquant"), ("Password missed", "Mot de passe manquant"),
@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Chat", "Discuter"), ("Chat", "Discuter"),
("Total", "Total"), ("Total", "Total"),
("items", "éléments"), ("items", "éléments"),
("Selected", "Sélectionné"), ("Selected", "Sélectionné(s)"),
("Screen Capture", "Capture d'écran"), ("Screen Capture", "Capture d'écran"),
("Input Control", "Contrôle de saisie"), ("Input Control", "Contrôle de saisie"),
("Audio Capture", "Capture audio"), ("Audio Capture", "Capture audio"),
@ -301,9 +301,9 @@ 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", "Machine distante refusée"), ("Peer denied", "Appareil distant refusé"),
("Please install plugins", "Veuillez installer les plugins"), ("Please install plugins", "Veuillez installer les plugins"),
("Peer exit", "Machine distante déconnectée"), ("Peer exit", "Appareil distant déconnecté"),
("Failed to turn off", "Échec de la désactivation"), ("Failed to turn off", "Échec de la désactivation"),
("Turned off", "Désactivé"), ("Turned off", "Désactivé"),
("In privacy mode", "en mode privé"), ("In privacy mode", "en mode privé"),
@ -382,7 +382,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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..."),
("elevated_foreground_window_tip", "La fenêtre actuelle que la machine distante nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."), ("elevated_foreground_window_tip", "La fenêtre actuelle que l'appareil distant nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."),
("Disconnected", "Déconnecté"), ("Disconnected", "Déconnecté"),
("Other", "Divers"), ("Other", "Divers"),
("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"),
@ -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 (côté machine distante)."), ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté appareil distant)."),
("Show RustDesk", "Afficher RustDesk"), ("Show RustDesk", "Afficher RustDesk"),
("This PC", "Ce PC"), ("This PC", "Ce PC"),
("or", "ou"), ("or", "ou"),
@ -454,7 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Appel voix"), ("Voice call", "Appel voix"),
("Text chat", "Conversation textuelfle"), ("Text chat", "Conversation textuelfle"),
("Stop voice call", "Stopper l'appel voix"), ("Stop voice call", "Stopper l'appel voix"),
("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche pair."), ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche appareils distants."),
("Reconnect", "Se reconnecter"), ("Reconnect", "Se reconnecter"),
("Codec", "Codec"), ("Codec", "Codec"),
("Resolution", "Résolution"), ("Resolution", "Résolution"),
@ -470,14 +470,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Maximize", "Maximiser"), ("Maximize", "Maximiser"),
("Your Device", "Votre appareil"), ("Your Device", "Votre appareil"),
("empty_recent_tip", "Oups, pas de sessions récentes!\nIl est temps d'en prévoir une nouvelle."), ("empty_recent_tip", "Oups, pas de sessions récentes!\nIl est temps d'en prévoir une nouvelle."),
("empty_favorite_tip", "Vous n'avez pas encore de pairs favoris?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-le à vos favoris!"), ("empty_favorite_tip", "Vous n'avez pas encore d'appareils distants favorits?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-la à vos favoris!"),
("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore de pairs découverts."), ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore d'appareil réseaux local découverts."),
("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun pair répertorié dans votre carnet d'adresses."), ("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun appareil distant répertorié dans votre carnet d'adresses."),
("eg: admin", "ex: admin"), ("eg: admin", "ex: admin"),
("Empty Username", "Nom d'utilisation non spécifié"), ("Empty Username", "Nom d'utilisation non spécifié"),
("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 de l'appareil distant."),
("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"),
@ -498,8 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Fingerprint", "Empreinte digitale"), ("Fingerprint", "Empreinte digitale"),
("Copy Fingerprint", "Copier empreinte digitale"), ("Copy Fingerprint", "Copier empreinte digitale"),
("no fingerprints", "Pas d'empreintes digitales"), ("no fingerprints", "Pas d'empreintes digitales"),
("Select a peer", "Sélectionnez la machine distante"), ("Select a peer", "Sélectionnez l'appareil distant"),
("Select peers", "Sélectionnez des machines distantes"), ("Select peers", "Sélectionnez des appareils distants"),
("Plugins", "Plugins"), ("Plugins", "Plugins"),
("Uninstall", "Désinstaller"), ("Uninstall", "Désinstaller"),
("Update", "Mise à jour"), ("Update", "Mise à jour"),
@ -523,7 +523,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop", "Stopper"), ("Stop", "Stopper"),
("exceed_max_devices", "Vous avez atteint le nombre maximal d'appareils gérés."), ("exceed_max_devices", "Vous avez atteint le nombre maximal d'appareils gérés."),
("Sync with recent sessions", "Synchroniser avec les sessions récentes"), ("Sync with recent sessions", "Synchroniser avec les sessions récentes"),
("Sort tags", "Trier les Tags"), ("Sort tags", "Trier les étiquettes"),
("Open connection in new tab", "Ouvrir la connexion dans un nouvel onglet"), ("Open connection in new tab", "Ouvrir la connexion dans un nouvel onglet"),
("Move tab to new window", "Déplacer l'onglet vers une nouvelle fenêtre"), ("Move tab to new window", "Déplacer l'onglet vers une nouvelle fenêtre"),
("Can not be empty", "Ne peux pas être vide"), ("Can not be empty", "Ne peux pas être vide"),
@ -534,9 +534,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Grid View", "Vue Grille"), ("Grid View", "Vue Grille"),
("List View", "Vue Liste"), ("List View", "Vue Liste"),
("Select", "Sélectionner"), ("Select", "Sélectionner"),
("Toggle Tags", "Basculer vers les Tags"), ("Toggle Tags", "Basculer vers les étiquettes"),
("pull_ab_failed_tip", "Impossible d'actualiser le carnet d'adresses"), ("pull_ab_failed_tip", "Impossible d'actualiser le carnet d'adresses"),
("push_ab_failed_tip", "Échec de la synchronisation du carnet d'adresses"), ("push_ab_failed_tip", "Échec de la synchronisation du carnet d'adresses"),
("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés avec le carnet d'adresses."), ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés avec le carnet d'adresses."),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "Gagal memuat ulang buku alamat"), ("pull_ab_failed_tip", "Gagal memuat ulang buku alamat"),
("push_ab_failed_tip", "Gagal menyinkronkan buku alamat ke server"), ("push_ab_failed_tip", "Gagal menyinkronkan buku alamat ke server"),
("synced_peer_readded_tip", "Perangkat yang terdaftar dalam sesi-sesi terbaru akan di-sinkronkan kembali ke buku alamat."), ("synced_peer_readded_tip", "Perangkat yang terdaftar dalam sesi-sesi terbaru akan di-sinkronkan kembali ke buku alamat."),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "Impossibile aggiornare la rubrica"), ("pull_ab_failed_tip", "Impossibile aggiornare la rubrica"),
("push_ab_failed_tip", "Impossibile sincronizzare la rubrica con il server"), ("push_ab_failed_tip", "Impossibile sincronizzare la rubrica con il server"),
("synced_peer_readded_tip", "I dispositivi presenti nelle sessioni recenti saranno sincronizzati di nuovo nella rubrica."), ("synced_peer_readded_tip", "I dispositivi presenti nelle sessioni recenti saranno sincronizzati di nuovo nella rubrica."),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "Невозможно обновить адресную книгу"), ("pull_ab_failed_tip", "Невозможно обновить адресную книгу"),
("push_ab_failed_tip", "Невозможно синхронизировать адресную книгу с сервером"), ("push_ab_failed_tip", "Невозможно синхронизировать адресную книгу с сервером"),
("synced_peer_readded_tip", "Устройства, присутствовавшие в последних сеансах, будут синхронизированы с адресной книгой."), ("synced_peer_readded_tip", "Устройства, присутствовавшие в последних сеансах, будут синхронизированы с адресной книгой."),
("Change Color", "Изменить цвет"),
("Primary Color", "Основной цвет"),
("HSV Color", "HSV цвет"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", "未成功獲取地址簿"), ("pull_ab_failed_tip", "未成功獲取地址簿"),
("push_ab_failed_tip", "未成功上傳地址簿"), ("push_ab_failed_tip", "未成功上傳地址簿"),
("synced_peer_readded_tip", "最近會話中存在的設備將會被重新同步到地址簿。"), ("synced_peer_readded_tip", "最近會話中存在的設備將會被重新同步到地址簿。"),
("Change Color", "更改顏色"),
("Primary Color", "基本色"),
("HSV Color", "HSV 色"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -538,5 +538,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -146,7 +146,7 @@ async fn connect_and_login(
return Ok(None); return Ok(None);
} }
Some(login_response::Union::PeerInfo(pi)) => { Some(login_response::Union::PeerInfo(pi)) => {
interface.handle_peer_info(pi, false); interface.handle_peer_info(pi);
break; break;
} }
_ => {} _ => {}

View File

@ -39,7 +39,7 @@ use hbb_common::{
tokio_util::codec::{BytesCodec, Framed}, tokio_util::codec::{BytesCodec, Framed},
}; };
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
use scrap::android::call_main_service_mouse_input; use scrap::android::call_main_service_pointer_input;
use serde_json::{json, value::Value}; use serde_json::{json, value::Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -1547,8 +1547,8 @@ impl Connection {
match msg.union { match msg.union {
Some(message::Union::MouseEvent(me)) => { Some(message::Union::MouseEvent(me)) => {
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = call_main_service_mouse_input(me.mask, me.x, me.y) { if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) {
log::debug!("call_main_service_mouse_input fail:{}", e); log::debug!("call_main_service_pointer_input fail:{}", e);
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.peer_keyboard_enabled() { if self.peer_keyboard_enabled() {
@ -1560,8 +1560,35 @@ impl Connection {
self.input_mouse(me, self.inner.id()); self.input_mouse(me, self.inner.id());
} }
} }
Some(message::Union::PointerDeviceEvent(pde)) => Some(message::Union::PointerDeviceEvent(pde)) => {
{ #[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = match pde.union {
Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union {
Some(touch_event::Union::PanStart(pan_start)) => {
call_main_service_pointer_input(
"touch",
4,
pan_start.x,
pan_start.y,
)
}
Some(touch_event::Union::PanUpdate(pan_update)) => {
call_main_service_pointer_input(
"touch",
5,
pan_update.x,
pan_update.y,
)
}
Some(touch_event::Union::PanEnd(pan_end)) => {
call_main_service_pointer_input("touch", 6, pan_end.x, pan_end.y)
}
_ => Ok(()),
},
_ => Ok(()),
} {
log::debug!("call_main_service_pointer_input fail:{}", e);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.peer_keyboard_enabled() { if self.peer_keyboard_enabled() {
MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst);

View File

@ -325,6 +325,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
// for tmp use, without real conn id // for tmp use, without real conn id
let mut write_jobs: Vec<fs::TransferJob> = Vec::new(); let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
#[cfg(windows)]
let is_authorized = self.cm.is_authorized(self.conn_id); let is_authorized = self.cm.is_authorized(self.conn_id);
#[cfg(windows)] #[cfg(windows)]

View File

@ -594,7 +594,7 @@ pub fn current_is_wayland() -> bool {
#[inline] #[inline]
pub fn get_new_version() -> String { pub fn get_new_version() -> String {
hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) (*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string()
} }
#[inline] #[inline]

View File

@ -48,17 +48,6 @@ pub static IS_IN: AtomicBool = AtomicBool::new(false);
const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15; const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15;
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Default)]
pub struct CacheFlutter {
pub pi: PeerInfo,
pub sp: Option<SwitchDisplay>,
pub cursor_data: HashMap<u64, CursorData>,
pub cursor_id: u64,
pub is_secured_direct: Option<(bool, bool)>,
}
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Session<T: InvokeUiSession> { pub struct Session<T: InvokeUiSession> {
pub session_id: SessionID, // different from the one in LoginConfigHandler, used for flutter UI message pass pub session_id: SessionID, // different from the one in LoginConfigHandler, used for flutter UI message pass
@ -73,9 +62,6 @@ pub struct Session<T: InvokeUiSession> {
pub server_file_transfer_enabled: Arc<RwLock<bool>>, pub server_file_transfer_enabled: Arc<RwLock<bool>>,
pub server_clipboard_enabled: Arc<RwLock<bool>>, pub server_clipboard_enabled: Arc<RwLock<bool>>,
pub last_change_display: Arc<Mutex<ChangeDisplayRecord>>, pub last_change_display: Arc<Mutex<ChangeDisplayRecord>>,
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub cache_flutter: Arc<RwLock<CacheFlutter>>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -724,6 +710,49 @@ impl<T: InvokeUiSession> Session<T> {
send_pointer_device_event(evt, alt, ctrl, shift, command, self); send_pointer_device_event(evt, alt, ctrl, shift, command, self);
} }
pub fn send_touch_pan_event(
&self,
event: &str,
x: i32,
y: i32,
alt: bool,
ctrl: bool,
shift: bool,
command: bool,
) {
let mut touch_evt = TouchEvent::new();
match event {
"pan_start" => {
touch_evt.set_pan_start(TouchPanStart {
x,
y,
..Default::default()
});
}
"pan_update" => {
touch_evt.set_pan_update(TouchPanUpdate {
x,
y,
..Default::default()
});
}
"pan_end" => {
touch_evt.set_pan_end(TouchPanEnd {
x,
y,
..Default::default()
});
}
_ => {
log::warn!("unknown touch pan event: {}", event);
return;
}
};
let mut evt = PointerDeviceEvent::new();
evt.set_touch_event(touch_evt);
send_pointer_device_event(evt, alt, ctrl, shift, command, self);
}
pub fn send_mouse( pub fn send_mouse(
&self, &self,
mask: i32, mask: i32,
@ -1095,7 +1124,7 @@ impl<T: InvokeUiSession> Interface for Session<T> {
handle_login_error(self.lc.clone(), err, self) handle_login_error(self.lc.clone(), err, self)
} }
fn handle_peer_info(&mut self, mut pi: PeerInfo, is_cached_pi: bool) { fn handle_peer_info(&mut self, mut pi: PeerInfo) {
log::debug!("handle_peer_info :{:?}", pi); log::debug!("handle_peer_info :{:?}", pi);
pi.username = self.lc.read().unwrap().get_username(&pi); pi.username = self.lc.read().unwrap().get_username(&pi);
if pi.current_display as usize >= pi.displays.len() { if pi.current_display as usize >= pi.displays.len() {
@ -1116,12 +1145,10 @@ impl<T: InvokeUiSession> Interface for Session<T> {
self.msgbox("error", "Remote Error", "No Display", ""); self.msgbox("error", "Remote Error", "No Display", "");
return; return;
} }
if !is_cached_pi { self.try_change_init_resolution(pi.current_display);
self.try_change_init_resolution(pi.current_display); let p = self.lc.read().unwrap().should_auto_login();
let p = self.lc.read().unwrap().should_auto_login(); if !p.is_empty() {
if !p.is_empty() { input_os_password(p, true, self.clone());
input_os_password(p, true, self.clone());
}
} }
let current = &pi.displays[pi.current_display as usize]; let current = &pi.displays[pi.current_display as usize];
self.set_display( self.set_display(
@ -1222,23 +1249,6 @@ impl<T: InvokeUiSession> Session<T> {
pub fn ctrl_alt_del(&self) { pub fn ctrl_alt_del(&self) {
self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del());
} }
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn restore_flutter_cache(&mut self) {
if let Some((is_secured, direct)) = self.cache_flutter.read().unwrap().is_secured_direct {
self.set_connection_type(is_secured, direct);
}
let pi = self.cache_flutter.read().unwrap().pi.clone();
self.handle_peer_info(pi, true);
if let Some(sp) = self.cache_flutter.read().unwrap().sp.as_ref() {
self.handle_peer_switch_display(sp);
}
for (_, cd) in self.cache_flutter.read().unwrap().cursor_data.iter() {
self.set_cursor_data(cd.clone());
}
self.set_cursor_id(self.cache_flutter.read().unwrap().cursor_id.to_string());
}
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]