Compare commits

..

12 Commits

Author SHA1 Message Date
rustdesk
32bcbbdb2d review fix 2026-06-17 16:27:36 +08:00
rustdesk
8382f1b598 review fix 2026-06-17 16:11:07 +08:00
rustdesk
ce9ae753e2 autocomplete online 2026-06-17 04:12:04 +08:00
rustdesk
bf206dc309 fixing https://github.com/rustdesk/rustdesk/issues/15293 2026-06-16 11:36:53 +08:00
fufesou
6d116cf1c9 fix(clipboard): Windows DIB images, fill missing alpha (#15296)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-16 11:29:53 +08:00
RustDesk
1fc33218dc Revert "fix(iPad): keep touch gestures with external mouse (#14652)" (#15288)
This reverts commit 5b7ad339b8.
2026-06-15 15:13:54 +08:00
21pages
b73e5bbfa0 opt: release clipboard config lock before updates (#15277)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-14 17:27:19 +08:00
fufesou
78533e428e feat: theme logo (#15268)
* feat: theme logo

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

* perf(flutter): cache theme logo asset resolution

* Potential fix for pull request finding

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-14 13:47:39 +08:00
fufesou
cc7fe4efdc Fix/generate py target injection (#15248)
* fix: generate.py, target injection

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

* refact: logs

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

* Potential fix for pull request finding

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

* Update port_forward.rs

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-11 23:09:34 +08:00
littlestejan
84af60c07e Fix clipboard synchronization not fully disabled in View Only mode (#15224)
* fix: view-only clipboard sync

Signed-off-by: Setani <little_stejan@hotmail.com>

* fix: gate Android MultiClipboards handling with clipboard permissions

Signed-off-by: Setani <little_stejan@hotmail.com>

---------

Signed-off-by: Setani <little_stejan@hotmail.com>
2026-06-10 07:42:58 +08:00
fufesou
6426269d41 Refact/printer driver default unchecked (#15191)
* refact: installation, printer driver, default unchecked

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

* refact: silent install, get option from the reg values

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

* refact: silent install, arg printer=[0|1]

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-06 08:51:08 +08:00
dependabot[bot]
7c41f993fe Git submodule: Bump libs/hbb_common from df6badc to 387603f (#15189)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `df6badc` to `387603f`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](df6badca5b...387603f47c)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 387603f47cbb15c0d3dc3d67ae3396d3eb707daf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:04:02 +08:00
28 changed files with 749 additions and 197 deletions

2
Cargo.lock generated
View File

@@ -292,7 +292,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.4.0" version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914" source = "git+https://github.com/rustdesk-org/arboard#c7d5781f563176df9efd8df6287e823fb1b9bed5"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics 0.23.2", "core-graphics 0.23.2",

View File

@@ -3713,14 +3713,54 @@ Widget loadPowered(BuildContext context) {
).marginOnly(top: 6); ).marginOnly(top: 6);
} }
// max 300 x 60 const _kDefaultLogoAsset = 'assets/logo.png';
Widget loadLogo() { const _kLightLogoAsset = 'assets/logo_light.png';
return FutureBuilder<ByteData>( const _kDarkLogoAsset = 'assets/logo_dark.png';
future: rootBundle.load('assets/logo.png'),
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) { List<String> _logoAssetCandidatesForBrightness(Brightness brightness) {
if (snapshot.hasData) { return brightness == Brightness.dark
? [_kDarkLogoAsset, _kDefaultLogoAsset]
: [_kLightLogoAsset, _kDefaultLogoAsset];
}
Future<String?> _resolveLogoAsset(Brightness brightness) async {
for (final asset in _logoAssetCandidatesForBrightness(brightness)) {
try {
await rootBundle.load(asset);
return asset;
} on FlutterError {
continue;
}
}
return null;
}
class _Logo extends StatefulWidget {
const _Logo();
@override
State<_Logo> createState() => _LogoState();
}
class _LogoState extends State<_Logo> {
final Map<Brightness, Future<String?>> _logoFutures = {};
Future<String?> _logoFutureFor(Brightness brightness) {
return _logoFutures.putIfAbsent(
brightness,
() => _resolveLogoAsset(brightness),
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
future: _logoFutureFor(Theme.of(context).brightness),
builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
final asset = snapshot.data;
if (asset != null) {
final image = Image.asset( final image = Image.asset(
'assets/logo.png', asset,
fit: BoxFit.contain, fit: BoxFit.contain,
errorBuilder: (ctx, error, stackTrace) { errorBuilder: (ctx, error, stackTrace) {
return Container(); return Container();
@@ -3732,9 +3772,14 @@ Widget loadLogo() {
).marginOnly(left: 12, right: 12, top: 12); ).marginOnly(left: 12, right: 12, top: 12);
} }
return const Offstage(); return const Offstage();
}); },
);
}
} }
// max 300 x 60
Widget loadLogo() => const _Logo();
Widget loadIcon(double size) { Widget loadIcon(double size) {
return Image.asset('assets/icon.png', return Image.asset('assets/icon.png',
width: size, width: size,

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import '../../../models/platform_model.dart'; import '../../../models/platform_model.dart';
@@ -5,27 +8,136 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
@visibleForTesting
List<Peer> mergeAutocompletePeers({
Iterable<Peer> addressBookPeers = const [],
Iterable<Peer> groupPeers = const [],
Iterable<Peer> lanPeers = const [],
Iterable<Peer> recentPeers = const [],
Iterable<String> restRecentPeerIds = const [],
}) {
final combinedPeers = <String, Peer>{};
void addPeer(Peer peer) {
if (peer.id.isEmpty) {
return;
}
final existingPeer = combinedPeers[peer.id];
if (existingPeer == null) {
combinedPeers[peer.id] = Peer.copy(peer);
} else if (peer.online) {
existingPeer.online = true;
}
}
for (final peer in addressBookPeers) {
addPeer(peer);
}
for (final peer in groupPeers) {
addPeer(peer);
}
for (final peer in lanPeers) {
addPeer(peer);
}
for (final peer in recentPeers) {
addPeer(peer);
}
for (final id in restRecentPeerIds) {
if (id.isNotEmpty && !combinedPeers.containsKey(id)) {
combinedPeers[id] = Peer.fromJson({'id': id});
}
}
return combinedPeers.values.toList(growable: false);
}
@visibleForTesting
bool updateAutocompletePeerOnlineStates(
List<Peer> peers, {
required Set<String> onlines,
required Set<String> offlines,
}) {
var changed = false;
for (final peer in peers) {
if (onlines.contains(peer.id)) {
if (!peer.online) {
peer.online = true;
changed = true;
}
} else if (offlines.contains(peer.id)) {
if (peer.online) {
peer.online = false;
changed = true;
}
}
}
return changed;
}
@visibleForTesting
List<String> autocompleteOnlineQueryIds(
Iterable<Peer> options, {
required int limit,
}) {
final ids = <String>[];
final seenIds = <String>{};
for (final peer in options) {
if (peer.id.isEmpty || seenIds.contains(peer.id)) {
continue;
}
seenIds.add(peer.id);
ids.add(peer.id);
if (ids.length >= limit) {
break;
}
}
return ids;
}
class AllPeersLoader { class AllPeersLoader {
List<Peer> peers = []; List<Peer> peers = [];
bool _isPeersLoading = false; bool _isPeersLoading = false;
bool _isPeersLoaded = false; bool _isPeersLoaded = false;
Set<String> _lastQueryOnlineIds = {};
DateTime _lastQueryOnlineTime = DateTime.fromMillisecondsSinceEpoch(0);
Timer? _queryOnlineTimer;
List<Peer> _lastQueryOnlineOptions = const [];
Set<String> _lastOnlineIds = {};
Set<String> _lastOfflineIds = {};
final Future<void> Function(List<String> ids) _queryOnlines;
final Duration _queryOnlineDebounce;
void Function(VoidCallback)? _setState;
bool _isCleared = false;
final String _listenerKey = 'AllPeersLoader'; final String _listenerKey = 'AllPeersLoader';
static const String _cbQueryOnlines = 'callback_query_onlines';
late void Function(VoidCallback) setState; static const Duration _queryOnlineInterval = Duration(seconds: 5);
static const Duration _defaultQueryOnlineDebounce =
Duration(milliseconds: 300);
static const int _maxQueryOnlineOptions = 20;
bool get needLoad => !_isPeersLoaded && !_isPeersLoading; bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
bool get isPeersLoaded => _isPeersLoaded; bool get isPeersLoaded => _isPeersLoaded;
AllPeersLoader(); AllPeersLoader({
@visibleForTesting Future<void> Function(List<String> ids)? queryOnlines,
@visibleForTesting Duration? queryOnlineDebounce,
}) : _queryOnlines = queryOnlines ?? ((ids) => bind.queryOnlines(ids: ids)),
_queryOnlineDebounce =
queryOnlineDebounce ?? _defaultQueryOnlineDebounce;
void init(void Function(VoidCallback) setState) { void init(void Function(VoidCallback) setState) {
this.setState = setState; _setState = setState;
_isCleared = false;
gFFI.recentPeersModel.addListener(_mergeAllPeers); gFFI.recentPeersModel.addListener(_mergeAllPeers);
gFFI.lanPeersModel.addListener(_mergeAllPeers); gFFI.lanPeersModel.addListener(_mergeAllPeers);
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
platformFFI.registerEventHandler(_cbQueryOnlines, _listenerKey,
(evt) async {
_updateOnlineState(evt);
});
} }
void clear() { void clear() {
@@ -33,6 +145,11 @@ class AllPeersLoader {
gFFI.lanPeersModel.removeListener(_mergeAllPeers); gFFI.lanPeersModel.removeListener(_mergeAllPeers);
gFFI.abModel.removePeerUpdateListener(_listenerKey); gFFI.abModel.removePeerUpdateListener(_listenerKey);
gFFI.groupModel.removePeerUpdateListener(_listenerKey); gFFI.groupModel.removePeerUpdateListener(_listenerKey);
platformFFI.unregisterEventHandler(_cbQueryOnlines, _listenerKey);
_queryOnlineTimer?.cancel();
_lastQueryOnlineOptions = const [];
_setState = null;
_isCleared = true;
} }
Future<void> getAllPeers() async { Future<void> getAllPeers() async {
@@ -59,50 +176,106 @@ class AllPeersLoader {
} }
void _mergeAllPeers() { void _mergeAllPeers() {
Map<String, dynamic> combinedPeers = {}; if (_isCleared) {
for (var p in gFFI.abModel.allPeers()) { return;
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
} }
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { peers = mergeAutocompletePeers(
if (!combinedPeers.containsKey(p.id)) { addressBookPeers: gFFI.abModel.allPeers(),
combinedPeers[p.id] = p.toJson(); groupPeers: gFFI.groupModel.peers,
} lanPeers: gFFI.lanPeersModel.peers,
} recentPeers: gFFI.recentPeersModel.peers,
restRecentPeerIds: gFFI.recentPeersModel.restPeerIds,
List<Peer> parsedPeers = []; );
for (var peer in combinedPeers.values) { _applyLastOnlineState(peers);
parsedPeers.add(Peer.fromJson(peer)); _scheduleSetState(() {
}
Set<String> peerIds = combinedPeers.keys.toSet();
for (final peer in gFFI.lanPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (final peer in gFFI.recentPeersModel.peers) {
if (!peerIds.contains(peer.id)) {
parsedPeers.add(peer);
peerIds.add(peer.id);
}
}
for (final id in gFFI.recentPeersModel.restPeerIds) {
if (!peerIds.contains(id)) {
parsedPeers.add(Peer.fromJson({'id': id}));
peerIds.add(id);
}
}
peers = parsedPeers;
setState(() {
_isPeersLoading = false; _isPeersLoading = false;
_isPeersLoaded = true; _isPeersLoaded = true;
}); });
} }
void _updateOnlineState(Map<String, dynamic> evt) {
if (_isCleared) {
return;
}
_lastOnlineIds = _splitPeerIds(evt['onlines']);
_lastOfflineIds = _splitPeerIds(evt['offlines']);
final peersChanged = _applyLastOnlineState(peers);
final optionsChanged = _applyLastOnlineState(_lastQueryOnlineOptions);
if (peersChanged || optionsChanged) {
_scheduleSetState(() {});
}
}
void _scheduleSetState(VoidCallback callback) {
if (_isCleared) {
return;
}
final setState = _setState;
if (setState == null) {
callback();
} else {
setState(callback);
}
}
bool _applyLastOnlineState(List<Peer> peers) {
return updateAutocompletePeerOnlineStates(
peers,
onlines: _lastOnlineIds,
offlines: _lastOfflineIds,
);
}
Set<String> _splitPeerIds(dynamic ids) {
if (ids is! String || ids.isEmpty) {
return {};
}
return ids.split(',').where((id) => id.isNotEmpty).toSet();
}
void queryOnlines(Iterable<Peer> options) {
if (_isCleared) {
return;
}
_lastQueryOnlineOptions = options.toList(growable: false);
final ids = autocompleteOnlineQueryIds(
_lastQueryOnlineOptions,
limit: _maxQueryOnlineOptions,
).toSet();
_queryOnlineTimer?.cancel();
_queryOnlineTimer = null;
if (ids.isEmpty) {
return;
}
final now = DateTime.now();
if (setEquals(ids, _lastQueryOnlineIds) &&
now.difference(_lastQueryOnlineTime) < _queryOnlineInterval) {
return;
}
_queryOnlineTimer = Timer(_queryOnlineDebounce, () async {
try {
await _queryOnlines(ids.toList(growable: false));
if (_isCleared) {
return;
}
_lastQueryOnlineIds = ids;
_lastQueryOnlineTime = DateTime.now();
} catch (e) {
debugPrint('query autocomplete online state failed: $e');
}
});
}
@visibleForTesting
void updateOnlineStateForTesting(Map<String, dynamic> evt) {
_updateOnlineState(evt);
}
@visibleForTesting
bool applyLastOnlineStateForTesting(List<Peer> peers) {
return _applyLastOnlineState(peers);
}
} }
class AutocompletePeerTile extends StatefulWidget { class AutocompletePeerTile extends StatefulWidget {

View File

@@ -393,6 +393,7 @@ class DialogTextField extends StatelessWidget {
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final int? maxLength; final int? maxLength;
final bool literalInput;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -411,6 +412,7 @@ class DialogTextField extends StatelessWidget {
this.keyboardType, this.keyboardType,
this.inputFormatters, this.inputFormatters,
this.maxLength, this.maxLength,
this.literalInput = false,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -435,7 +437,17 @@ class DialogTextField extends StatelessWidget {
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType ??
(literalInput ? TextInputType.visiblePassword : null),
textCapitalization: TextCapitalization.none,
autocorrect: !literalInput,
enableSuggestions: !literalInput,
smartDashesType: literalInput ? SmartDashesType.disabled : null,
smartQuotesType: literalInput ? SmartQuotesType.disabled : null,
enableIMEPersonalizedLearning: !literalInput,
spellCheckConfiguration: literalInput
? const SpellCheckConfiguration.disabled()
: null,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
maxLength: maxLength, maxLength: maxLength,
), ),

View File

@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
// Official // Official
TapGestureRecognizer: TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer( () => TapGestureRecognizer(), (instance) {
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance instance
..onTapDown = onTapDown ..onTapDown = onTapDown
..onTapUp = onTapUp ..onTapUp = onTapUp
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
}), }),
DoubleTapGestureRecognizer: DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>( GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer( () => DoubleTapGestureRecognizer(), (instance) {
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance instance
..onDoubleTapDown = onDoubleTapDown ..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap; ..onDoubleTap = onDoubleTap;
}), }),
LongPressGestureRecognizer: LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer( () => LongPressGestureRecognizer(), (instance) {
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance instance
..onLongPressDown = onLongPressDown ..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp ..onLongPressUp = onLongPressUp
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
// Customized // Customized
HoldTapMoveGestureRecognizer: HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>( GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer( () => HoldTapMoveGestureRecognizer(),
supportedDevices: kTouchBasedDeviceKinds,
),
(instance) => instance (instance) => instance
..onHoldDragStart = onHoldDragStart ..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate ..onHoldDragUpdate = onHoldDragUpdate
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
..onHoldDragEnd = onHoldDragEnd), ..onHoldDragEnd = onHoldDragEnd),
DoubleFinerTapGestureRecognizer: DoubleFinerTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>( GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer( () => DoubleFinerTapGestureRecognizer(), (instance) {
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance instance
..onDoubleFinerTap = onDoubleFinerTap ..onDoubleFinerTap = onDoubleFinerTap
..onDoubleFinerTapDown = onDoubleFinerTapDown; ..onDoubleFinerTapDown = onDoubleFinerTapDown;
}), }),
CustomTouchGestureRecognizer: CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>( GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer( () => CustomTouchGestureRecognizer(), (instance) {
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
instance.onOneFingerPanStart = instance.onOneFingerPanStart =
(DragStartDetails d) => onOneFingerPanStart(context, d); (DragStartDetails d) => onOneFingerPanStart(context, d);
instance instance

View File

@@ -398,6 +398,7 @@ class _ConnectionPageState extends State<ConnectionPage>
.contains(textToFind) || .contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind)) peer.alias.toLowerCase().contains(textToFind))
.toList(); .toList();
_allPeersLoader.queryOnlines(_autocompleteOpts);
} }
return _autocompleteOpts; return _autocompleteOpts;
}, },

View File

@@ -65,7 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
late final TextEditingController controller; late final TextEditingController controller;
final RxBool startmenu = true.obs; final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs; final RxBool desktopicon = true.obs;
final RxBool printer = true.obs; final RxBool printer = false.obs;
final RxBool showProgress = false.obs; final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs; final RxBool btnEnabled = true.obs;
@@ -80,7 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
final installOptions = jsonDecode(bind.installInstallOptions()); final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
printer.value = installOptions['PRINTER'] != '0'; printer.value = installOptions['PRINTER'] == '1';
} }
@override @override

View File

@@ -207,6 +207,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
.contains(textToFind) || .contains(textToFind) ||
peer.alias.toLowerCase().contains(textToFind)) peer.alias.toLowerCase().contains(textToFind))
.toList(); .toList();
_allPeersLoader.queryOnlines(_autocompleteOpts);
} }
return _autocompleteOpts; return _autocompleteOpts;
}, },

View File

@@ -517,10 +517,12 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
} }
return Container( return Container(
color: MyTheme.canvasColor, color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion( child: inputModel.isPhysicalMouse.value
child: getBodyForMobile(), ? getBodyForMobile()
ffi: gFFI, : RawTouchGestureDetectorRegion(
), child: getBodyForMobile(),
ffi: gFFI,
),
); );
}), }),
), ),

View File

@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State<ViewCameraPage>
} }
return Container( return Container(
color: MyTheme.canvasColor, color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion( child: inputModel.isPhysicalMouse.value
child: getBodyForMobile(), ? getBodyForMobile()
ffi: gFFI, : RawTouchGestureDetectorRegion(
isCamera: true, child: getBodyForMobile(),
), ffi: gFFI,
isCamera: true,
),
); );
}), }),
), ),

View File

@@ -117,13 +117,13 @@ void showServerSettingsWithValue(
), ),
SizedBox(width: 8), SizedBox(width: 8),
Expanded( Expanded(
child: TextFormField( child: serverSettingsTextFormField(
label: label,
controller: controller, controller: controller,
decoration: InputDecoration( errorMsg: errorMsg,
errorText: errorMsg.isEmpty ? null : errorMsg, contentPadding:
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 12),
EdgeInsets.symmetric(horizontal: 8, vertical: 12), showLabelText: false,
),
validator: validator, validator: validator,
autofocus: autofocus, autofocus: autofocus,
).workaroundFreezeLinuxMint(), ).workaroundFreezeLinuxMint(),
@@ -132,12 +132,10 @@ void showServerSettingsWithValue(
); );
} }
return TextFormField( return serverSettingsTextFormField(
label: label,
controller: controller, controller: controller,
decoration: InputDecoration( errorMsg: errorMsg,
labelText: label,
errorText: errorMsg.isEmpty ? null : errorMsg,
),
validator: validator, validator: validator,
).workaroundFreezeLinuxMint(); ).workaroundFreezeLinuxMint();
} }
@@ -209,6 +207,35 @@ void showServerSettingsWithValue(
}); });
} }
TextFormField serverSettingsTextFormField({
required String label,
required TextEditingController controller,
required String errorMsg,
String? Function(String?)? validator,
bool autofocus = false,
bool showLabelText = true,
EdgeInsetsGeometry? contentPadding,
}) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: showLabelText ? label : null,
errorText: errorMsg.isEmpty ? null : errorMsg,
contentPadding: contentPadding,
),
validator: validator,
autofocus: autofocus,
keyboardType: TextInputType.visiblePassword,
textCapitalization: TextCapitalization.none,
autocorrect: false,
enableSuggestions: false,
smartDashesType: SmartDashesType.disabled,
smartQuotesType: SmartQuotesType.disabled,
enableIMEPersonalizedLearning: false,
spellCheckConfiguration: const SpellCheckConfiguration.disabled(),
);
}
void setPrivacyModeDialog( void setPrivacyModeDialog(
OverlayDialogManager dialogManager, OverlayDialogManager dialogManager,
List<TToggleMenu> privacyModeList, List<TToggleMenu> privacyModeList,

View File

@@ -145,23 +145,26 @@ class Peer {
note == other.note; note == other.note;
} }
Peer.copy(Peer other) factory Peer.copy(Peer other) {
: this( final peer = Peer(
id: other.id, id: other.id,
hash: other.hash, hash: other.hash,
password: other.password, password: other.password,
username: other.username, username: other.username,
hostname: other.hostname, hostname: other.hostname,
platform: other.platform, platform: other.platform,
alias: other.alias, alias: other.alias,
tags: other.tags.toList(), tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay, forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort, rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername, rdpUsername: other.rdpUsername,
loginName: other.loginName, loginName: other.loginName,
device_group_name: other.device_group_name, device_group_name: other.device_group_name,
note: other.note, note: other.note,
sameServer: other.sameServer); sameServer: other.sameServer);
peer.online = other.online;
return peer;
}
} }
enum UpdateEvent { online, load } enum UpdateEvent { online, load }

View File

@@ -0,0 +1,148 @@
import 'package:flutter_hbb/common/widgets/autocomplete.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_test/flutter_test.dart';
Peer _peer({
required String id,
String alias = '',
String username = '',
String hostname = '',
bool online = false,
}) {
final peer = Peer(
id: id,
username: username,
hostname: hostname,
alias: alias,
platform: '',
tags: [],
hash: '',
password: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
loginName: '',
device_group_name: '',
note: '',
);
peer.online = online;
return peer;
}
void main() {
test('merged autocomplete peers keep address book metadata and online state',
() {
final peers = mergeAutocompletePeers(
addressBookPeers: [
_peer(id: '123456789', alias: 'Office PC', username: 'ab-user'),
],
lanPeers: [
_peer(id: '123456789', username: 'lan-user', online: true),
],
);
expect(peers, hasLength(1));
expect(peers.single.id, '123456789');
expect(peers.single.alias, 'Office PC');
expect(peers.single.username, 'ab-user');
expect(peers.single.online, isTrue);
});
test('peer copies preserve online state', () {
final peer = _peer(id: '987654321', online: true);
expect(Peer.copy(peer).online, isTrue);
});
test('online callbacks update autocomplete-only peers', () {
final peers = mergeAutocompletePeers(restRecentPeerIds: ['112233445']);
final changed = updateAutocompletePeerOnlineStates(
peers,
onlines: {'112233445'},
offlines: {},
);
expect(changed, isTrue);
expect(peers.single.online, isTrue);
});
test('online query ids are deduplicated and limited', () {
final peers = List.generate(
25,
(index) => _peer(id: index.toString()),
)..insert(1, _peer(id: '0'));
final ids = autocompleteOnlineQueryIds(peers, limit: 20);
expect(ids, hasLength(20));
expect(ids.first, '0');
expect(ids.where((id) => id == '0'), hasLength(1));
expect(ids.last, '19');
});
test('empty online query ids cancel pending debounce', () async {
final queriedIds = <List<String>>[];
final loader = AllPeersLoader(
queryOnlines: (ids) async {
queriedIds.add(ids);
},
queryOnlineDebounce: Duration(milliseconds: 1),
);
loader.queryOnlines([_peer(id: '123456789')]);
loader.queryOnlines([]);
await Future.delayed(Duration(milliseconds: 2));
expect(queriedIds, isEmpty);
});
test('failed online query enqueue does not suppress retry', () async {
var queryCount = 0;
final loader = AllPeersLoader(
queryOnlines: (ids) {
queryCount += 1;
return Future<void>.error(Exception('queue full'));
},
queryOnlineDebounce: Duration(milliseconds: 1),
);
loader.queryOnlines([_peer(id: '123456789')]);
await Future.delayed(Duration(milliseconds: 2));
loader.queryOnlines([_peer(id: '123456789')]);
await Future.delayed(Duration(milliseconds: 2));
expect(queryCount, 2);
});
test('online callback updates currently displayed options', () async {
final loader = AllPeersLoader(
queryOnlines: (ids) async {},
queryOnlineDebounce: Duration(milliseconds: 1),
);
final displayedOptions = [_peer(id: '123456789')];
loader.queryOnlines(displayedOptions);
loader.updateOnlineStateForTesting({
'onlines': '123456789',
'offlines': '',
});
expect(displayedOptions.single.online, isTrue);
await Future.delayed(Duration(milliseconds: 2));
});
test('cached online callback state is reapplied after peers merge', () {
final loader = AllPeersLoader();
loader.updateOnlineStateForTesting({
'onlines': '123456789',
'offlines': '',
});
final mergedPeers = [_peer(id: '123456789')];
loader.applyLastOnlineStateForTesting(mergedPeers);
expect(mergedPeers.single.online, isTrue);
});
}

63
flutter/test/cm_demo.dart Normal file
View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
final testClients = [
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false),
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false),
Client(2, false, false, false, "UserC", "331123123", true, false, false),
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false,
false)
];
/// flutter run -d {platform} -t test/cm_demo.dart to test cm
void main() async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
await windowManager.setSize(const Size(400, 600));
await windowManager.setAlignment(Alignment.topRight);
await initEnv(kAppTypeMain);
for (var client in testClients) {
gFFI.serverModel.clients.add(client);
gFFI.serverModel.tabController.add(TabInfo(
key: client.id.toString(),
label: client.name,
closable: false,
page: buildConnectionCard(client)));
}
runApp(GetMaterialApp(
debugShowCheckedModeBanner: false,
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage()));
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
// ensure initial window size to be changed
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
await Future.wait([
windowManager.setAlignment(Alignment.topRight),
windowManager.focus(),
windowManager.setOpacity(1)
]);
// ensure
windowManager.setAlignment(Alignment.topRight);
});
}

View File

@@ -1,62 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
final testClients = [ import 'cm_demo.dart' as cm_demo;
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false),
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false),
Client(2, false, false, false, "UserC", "331123123", true, false, false, false),
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false)
];
/// flutter run -d {platform} -t test/cm_test.dart to test cm void main() {
void main(List<String> args) async { test('connection manager demo clients match the current Client API', () {
isTest = true; expect(cm_demo.testClients, hasLength(4));
WidgetsFlutterBinding.ensureInitialized(); expect(cm_demo.testClients.map((client) => client.name), [
await windowManager.ensureInitialized(); 'UserAAAAAA',
await windowManager.setSize(const Size(400, 600)); 'UserBBBBB',
await windowManager.setAlignment(Alignment.topRight); 'UserC',
await initEnv(kAppTypeMain); 'UserDDDDDDDDDDDd',
for (var client in testClients) {
gFFI.serverModel.clients.add(client);
gFFI.serverModel.tabController.add(TabInfo(
key: client.id.toString(),
label: client.name,
closable: false,
page: buildConnectionCard(client)));
}
runApp(GetMaterialApp(
debugShowCheckedModeBanner: false,
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage()));
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
// ensure initial window size to be changed
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
await Future.wait([
windowManager.setAlignment(Alignment.topRight),
windowManager.focus(),
windowManager.setOpacity(1)
]); ]);
// ensure expect(
windowManager.setAlignment(Alignment.topRight); cm_demo.testClients.every(
(client) => client.keyboard && !client.clipboard && !client.audio),
isTrue,
);
}); });
} }

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
void main() {
testWidgets('DialogTextField can preserve literal input', (tester) async {
final controller = TextEditingController(text: 'P@ss1c1E=');
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DialogTextField(
title: 'Password',
controller: controller,
literalInput: true,
),
),
));
final textField = tester.widget<TextField>(find.byType(TextField));
expect(textField.controller, controller);
expect(textField.keyboardType, TextInputType.visiblePassword);
expect(textField.textCapitalization, TextCapitalization.none);
expect(textField.autocorrect, isFalse);
expect(textField.enableSuggestions, isFalse);
expect(textField.smartDashesType, SmartDashesType.disabled);
expect(textField.smartQuotesType, SmartQuotesType.disabled);
expect(textField.enableIMEPersonalizedLearning, isFalse);
expect(
textField.spellCheckConfiguration,
const SpellCheckConfiguration.disabled(),
);
});
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
void main() {
testWidgets('server settings text fields preserve literal input',
(tester) async {
final controller = TextEditingController(text: 'AbCdR1c1E=');
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: serverSettingsTextFormField(
label: 'Key',
controller: controller,
errorMsg: '',
autofocus: true,
),
),
));
final textField = tester.widget<TextField>(find.byType(TextField));
expect(textField.controller, controller);
expect(textField.autofocus, isTrue);
expect(textField.keyboardType, TextInputType.visiblePassword);
expect(textField.textCapitalization, TextCapitalization.none);
expect(textField.autocorrect, isFalse);
expect(textField.enableSuggestions, isFalse);
expect(textField.smartDashesType, SmartDashesType.disabled);
expect(textField.smartQuotesType, SmartQuotesType.disabled);
expect(textField.enableIMEPersonalizedLearning, isFalse);
expect(
textField.spellCheckConfiguration,
const SpellCheckConfiguration.disabled(),
);
});
}

View File

@@ -2,6 +2,7 @@
import os import os
import optparse import optparse
import subprocess
from hashlib import md5 from hashlib import md5
import brotli import brotli
import datetime import datetime
@@ -65,11 +66,15 @@ def write_app_metadata(output_folder: str):
print(f"App metadata has been written to {output_path}") print(f"App metadata has been written to {output_path}")
def build_portable(output_folder: str, target: str): def build_portable(output_folder: str, target: str):
os.chdir(output_folder) current_dir = os.getcwd()
if target: try:
os.system("cargo build --locked --release --target " + target) os.chdir(output_folder)
else: cmd = ["cargo", "build", "--locked", "--release"]
os.system("cargo build --locked --release") if target:
cmd.extend(["--target", target])
subprocess.run(cmd, check=True)
finally:
os.chdir(current_dir)
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py # Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe # Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe

View File

@@ -67,7 +67,7 @@
Some msi packages reset the `VersionNT` value to 1000 on Windows 10. Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html --> https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
<!-- Remote printer also works on Win8.1 in my test. --> <!-- Remote printer also works on Win8.1 in my test. -->
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;" /> <Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND (PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;)" />
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT &gt;= 603" /> <Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT &gt;= 603" />
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.--> <!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->

View File

@@ -4,17 +4,17 @@
<?include ..\Includes.wxi?> <?include ..\Includes.wxi?>
<!-- <!--
Properties and related actions for specifying whether to install start menu/desktop shortcuts. Properties and related actions for specifying whether to install shortcuts and the printer.
--> -->
<!-- These are the actual properties that get used in conditions to determine whether to <!-- These are the actual properties that get used in conditions to determine whether to
install start menu shortcuts, they are initialized with a default value to install shortcuts. install start menu shortcuts or the printer. Shortcut properties default to install;
They should not be set directly from the command line or registry, instead the CREATE* properties PRINTER defaults to not install. The CREATE* properties below update shortcut
below should be set, then they will update these properties with their values only if set. --> properties from command line, bundle, or registry values. -->
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="PRINTER" Value="1" Secure="yes"></Property> <Property Id="PRINTER" Secure="yes"></Property>
<!-- These properties get set from either the command line, bundle or registry value, <!-- These properties get set from either the command line, bundle or registry value,
if set they update the properties above with their value. --> if set they update the properties above with their value. -->
@@ -77,7 +77,11 @@
<!-- If a command line value or registry value was set, update the main properties with the value --> <!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" /> <SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" /> <SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="PRINTER" Value="" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" /> <!-- PRINTER defaults to empty now, so a saved or command-line INSTALLPRINTER=1
must explicitly enable the main PRINTER property. Non-truthy INSTALLPRINTER
values still clear PRINTER so upgrades preserve an explicit disabled choice. -->
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;" />
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</Fragment> </Fragment>
</Wix> </Wix>

View File

@@ -1426,7 +1426,11 @@ impl<T: InvokeUiSession> Remote<T> {
self.handler.set_cursor_position(cp); self.handler.set_cursor_position(cp);
} }
Some(message::Union::Clipboard(cb)) => { Some(message::Union::Clipboard(cb)) => {
if !self.handler.lc.read().unwrap().disable_clipboard.v { let clipboard_allowed = {
let lc = self.handler.lc.read().unwrap();
!lc.disable_clipboard.v && !lc.view_only.v
};
if clipboard_allowed {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(vec![cb], ClipboardSide::Client); update_clipboard(vec![cb], ClipboardSide::Client);
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
@@ -1445,7 +1449,11 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
Some(message::Union::MultiClipboards(_mcb)) => { Some(message::Union::MultiClipboards(_mcb)) => {
if !self.handler.lc.read().unwrap().disable_clipboard.v { let clipboard_allowed = {
let lc = self.handler.lc.read().unwrap();
!lc.disable_clipboard.v && !lc.view_only.v
};
if clipboard_allowed {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client); update_clipboard(_mcb.clipboards, ClipboardSide::Client);
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]

View File

@@ -262,11 +262,9 @@ pub fn core_main() -> Option<Vec<String>> {
if config::is_disable_installation() { if config::is_disable_installation() {
return None; return None;
} }
#[cfg(not(windows))] let (printer_override, debug) = parse_silent_install_args(&args);
let options = "desktopicon startmenu"; let options = platform::get_silent_install_options(printer_override);
#[cfg(windows)] let res = platform::install_me(options, "".to_owned(), true, debug);
let options = "desktopicon startmenu printer";
let res = platform::install_me(options, "".to_owned(), true, args.len() > 1);
let text = match res { let text = match res {
Ok(_) => translate("Installation Successful!".to_string()), Ok(_) => translate("Installation Successful!".to_string()),
Err(err) => { Err(err) => {
@@ -933,6 +931,23 @@ fn is_cli_setting_change_disabled() -> bool {
config::is_disable_settings() && !allow_command_line_settings config::is_disable_settings() && !allow_command_line_settings
} }
#[cfg(windows)]
fn parse_silent_install_args(args: &[String]) -> (Option<bool>, bool) {
let mut printer_override = None;
let mut debug = false;
for arg in args.iter().skip(1) {
match arg.as_str() {
"printer=1" => printer_override = Some(true),
"printer=0" => printer_override = Some(false),
"debug" => debug = true,
_ => {}
}
}
(printer_override, debug)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -326,12 +326,14 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
try_sync_peer_option(&session, &session_id, &value, None); try_sync_peer_option(&session, &session_id, &value, None);
} }
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { if sessions::get_session_by_session_id(&session_id).is_some()
&& (value == "disable-clipboard" || value == "view-only")
{
crate::flutter::update_text_clipboard_required(); crate::flutter::update_text_clipboard_required();
} }
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
if sessions::get_session_by_session_id(&session_id).is_some() if sessions::get_session_by_session_id(&session_id).is_some()
&& value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE && (value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only")
{ {
crate::flutter::update_file_clipboard_required(); crate::flutter::update_file_clipboard_required();
} }

View File

@@ -1324,6 +1324,23 @@ pub fn get_install_options() -> String {
serde_json::to_string(&opts).unwrap_or("{}".to_owned()) serde_json::to_string(&opts).unwrap_or("{}".to_owned())
} }
pub fn get_silent_install_options(printer_override: Option<bool>) -> &'static str {
let install_printer = match printer_override {
Some(override_value) => override_value,
None => {
let app_name = crate::get_app_name();
let subkey = format!(".{}", app_name.to_lowercase());
let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER);
printer.as_deref() == Some("1")
}
};
if install_printer && is_win_10_or_greater() {
"desktopicon startmenu printer"
} else {
"desktopicon startmenu"
}
}
// This function return Option<String>, because some registry value may be empty. // This function return Option<String>, because some registry value may be empty.
fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option<String> { fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option<String> {
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); let hkcr = RegKey::predef(HKEY_CLASSES_ROOT);

View File

@@ -30,7 +30,6 @@ fn run_rdp(port: u16) {
if !password.is_empty() { if !password.is_empty() {
args.push(format!("/pass:{}", password)); args.push(format!("/pass:{}", password));
} }
println!("{:?}", args);
std::process::Command::new("cmdkey") std::process::Command::new("cmdkey")
.args(&args) .args(&args)
.output() .output()

View File

@@ -2891,7 +2891,7 @@ impl Connection {
self.update_auto_disconnect_timer(); self.update_auto_disconnect_timer();
} }
Some(message::Union::Clipboard(cb)) => { Some(message::Union::Clipboard(cb)) => {
if self.clipboard { if self.clipboard_enabled() {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(vec![cb], ClipboardSide::Host); update_clipboard(vec![cb], ClipboardSide::Host);
// ios as the controlled side is actually not supported for now. // ios as the controlled side is actually not supported for now.
@@ -2919,12 +2919,12 @@ impl Connection {
} }
} }
Some(message::Union::MultiClipboards(_mcb)) => { Some(message::Union::MultiClipboards(_mcb)) => {
#[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard_enabled() {
if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Host); update_clipboard(_mcb.clipboards, ClipboardSide::Host);
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
} }
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
} }
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
Some(message::Union::Cliprdr(clip)) => { Some(message::Union::Cliprdr(clip)) => {

View File

@@ -175,13 +175,16 @@ impl SessionPermissionConfig {
*self.server_clipboard_enabled.read().unwrap() *self.server_clipboard_enabled.read().unwrap()
&& *self.server_keyboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap()
&& !self.lc.read().unwrap().disable_clipboard.v && !self.lc.read().unwrap().disable_clipboard.v
&& !self.lc.read().unwrap().view_only.v
} }
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
pub fn is_file_clipboard_required(&self) -> bool { pub fn is_file_clipboard_required(&self) -> bool {
let lc = self.lc.read().unwrap();
*self.server_keyboard_enabled.read().unwrap() *self.server_keyboard_enabled.read().unwrap()
&& *self.server_file_transfer_enabled.read().unwrap() && *self.server_file_transfer_enabled.read().unwrap()
&& self.lc.read().unwrap().enable_file_copy_paste.v && lc.enable_file_copy_paste.v
&& !lc.view_only.v
} }
} }
@@ -411,13 +414,16 @@ impl<T: InvokeUiSession> Session<T> {
*self.server_clipboard_enabled.read().unwrap() *self.server_clipboard_enabled.read().unwrap()
&& *self.server_keyboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap()
&& !self.lc.read().unwrap().disable_clipboard.v && !self.lc.read().unwrap().disable_clipboard.v
&& !self.lc.read().unwrap().view_only.v
} }
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
pub fn is_file_clipboard_required(&self) -> bool { pub fn is_file_clipboard_required(&self) -> bool {
let lc = self.lc.read().unwrap();
*self.server_keyboard_enabled.read().unwrap() *self.server_keyboard_enabled.read().unwrap()
&& *self.server_file_transfer_enabled.read().unwrap() && *self.server_file_transfer_enabled.read().unwrap()
&& self.lc.read().unwrap().enable_file_copy_paste.v && lc.enable_file_copy_paste.v
&& !lc.view_only.v
} }
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]