mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-18 14:24:54 +03:00
Compare commits
19 Commits
1.4.7
...
fuse-file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0625126e | ||
|
|
8a955888bf | ||
|
|
36e812e550 | ||
|
|
8baa995c7a | ||
|
|
f4a0535289 | ||
|
|
6665242edf | ||
|
|
3cdf1cce54 | ||
|
|
88ae00ba73 | ||
|
|
7c26575dbd | ||
|
|
93d064a9b0 | ||
|
|
bf206dc309 | ||
|
|
6d116cf1c9 | ||
|
|
1fc33218dc | ||
|
|
b73e5bbfa0 | ||
|
|
78533e428e | ||
|
|
cc7fe4efdc | ||
|
|
84af60c07e | ||
|
|
6426269d41 | ||
|
|
7c41f993fe |
2
.github/workflows/flutter-build.yml
vendored
2
.github/workflows/flutter-build.yml
vendored
@@ -39,7 +39,7 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.7"
|
||||
VERSION: "1.4.8"
|
||||
NDK_VERSION: "r28c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.7"
|
||||
VERSION: "1.4.8"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -292,7 +292,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.4.0"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#c7d5781f563176df9efd8df6287e823fb1b9bed5"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.23.2",
|
||||
@@ -3952,7 +3952,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#778df1f99597722473b29443bac22ae6c23946fe"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -7270,7 +7270,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.4.7"
|
||||
version = "1.4.8"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -7385,7 +7385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.7"
|
||||
version = "1.4.8"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.7"
|
||||
version = "1.4.8"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.7
|
||||
version: 1.4.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.7
|
||||
version: 1.4.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199"><path fill="#0089d6" d="M118.432 187.698c32.89-5.81 60.055-10.618 60.367-10.684l.568-.12-31.052-36.935c-17.078-20.314-31.051-37.014-31.051-37.11 0-.182 32.063-88.477 32.243-88.792.06-.105 21.88 37.567 52.893 91.32 29.035 50.323 52.973 91.815 53.195 92.203l.405.707-98.684-.012-98.684-.013 59.8-10.564zM0 176.435c0-.052 14.631-25.451 32.514-56.442l32.514-56.347 37.891-31.799C123.76 14.358 140.867.027 140.935.001c.069-.026-.205.664-.609 1.534s-18.919 40.582-41.145 88.25l-40.41 86.67-29.386.037c-16.162.02-29.385-.005-29.385-.057z"/></svg>
|
||||
|
Before Width: | Height: | Size: 604 B |
@@ -3713,14 +3713,54 @@ Widget loadPowered(BuildContext context) {
|
||||
).marginOnly(top: 6);
|
||||
}
|
||||
|
||||
// max 300 x 60
|
||||
Widget loadLogo() {
|
||||
return FutureBuilder<ByteData>(
|
||||
future: rootBundle.load('assets/logo.png'),
|
||||
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
const _kDefaultLogoAsset = 'assets/logo.png';
|
||||
const _kLightLogoAsset = 'assets/logo_light.png';
|
||||
const _kDarkLogoAsset = 'assets/logo_dark.png';
|
||||
|
||||
List<String> _logoAssetCandidatesForBrightness(Brightness brightness) {
|
||||
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(
|
||||
'assets/logo.png',
|
||||
asset,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (ctx, error, stackTrace) {
|
||||
return Container();
|
||||
@@ -3732,9 +3772,14 @@ Widget loadLogo() {
|
||||
).marginOnly(left: 12, right: 12, top: 12);
|
||||
}
|
||||
return const Offstage();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// max 300 x 60
|
||||
Widget loadLogo() => const _Logo();
|
||||
|
||||
Widget loadIcon(double size) {
|
||||
return Image.asset('assets/icon.png',
|
||||
width: size,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.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/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 {
|
||||
List<Peer> peers = [];
|
||||
|
||||
bool _isPeersLoading = 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';
|
||||
|
||||
late void Function(VoidCallback) setState;
|
||||
static const String _cbQueryOnlines = 'callback_query_onlines';
|
||||
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 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) {
|
||||
this.setState = setState;
|
||||
_setState = setState;
|
||||
_isCleared = false;
|
||||
gFFI.recentPeersModel.addListener(_mergeAllPeers);
|
||||
gFFI.lanPeersModel.addListener(_mergeAllPeers);
|
||||
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||
platformFFI.registerEventHandler(_cbQueryOnlines, _listenerKey,
|
||||
(evt) async {
|
||||
_updateOnlineState(evt);
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
@@ -33,6 +145,11 @@ class AllPeersLoader {
|
||||
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
|
||||
gFFI.abModel.removePeerUpdateListener(_listenerKey);
|
||||
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
|
||||
platformFFI.unregisterEventHandler(_cbQueryOnlines, _listenerKey);
|
||||
_queryOnlineTimer?.cancel();
|
||||
_lastQueryOnlineOptions = const [];
|
||||
_setState = null;
|
||||
_isCleared = true;
|
||||
}
|
||||
|
||||
Future<void> getAllPeers() async {
|
||||
@@ -59,50 +176,106 @@ class AllPeersLoader {
|
||||
}
|
||||
|
||||
void _mergeAllPeers() {
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
for (var p in gFFI.abModel.allPeers()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
if (_isCleared) {
|
||||
return;
|
||||
}
|
||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> parsedPeers = [];
|
||||
for (var peer in combinedPeers.values) {
|
||||
parsedPeers.add(Peer.fromJson(peer));
|
||||
}
|
||||
|
||||
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(() {
|
||||
peers = mergeAutocompletePeers(
|
||||
addressBookPeers: gFFI.abModel.allPeers(),
|
||||
groupPeers: gFFI.groupModel.peers,
|
||||
lanPeers: gFFI.lanPeersModel.peers,
|
||||
recentPeers: gFFI.recentPeersModel.peers,
|
||||
restRecentPeerIds: gFFI.recentPeersModel.restPeerIds,
|
||||
);
|
||||
_applyLastOnlineState(peers);
|
||||
_scheduleSetState(() {
|
||||
_isPeersLoading = false;
|
||||
_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 {
|
||||
|
||||
@@ -24,6 +24,35 @@ const kOpSvgList = [
|
||||
'microsoft'
|
||||
];
|
||||
|
||||
class _OidcProviderBranding {
|
||||
final String label;
|
||||
final String iconKey;
|
||||
|
||||
const _OidcProviderBranding({
|
||||
required this.label,
|
||||
required this.iconKey,
|
||||
});
|
||||
}
|
||||
|
||||
_OidcProviderBranding _oidcProviderBranding(String op) {
|
||||
switch (op.toLowerCase()) {
|
||||
case 'azure':
|
||||
return _OidcProviderBranding(
|
||||
label: 'Microsoft',
|
||||
iconKey: 'microsoft',
|
||||
);
|
||||
default:
|
||||
return _OidcProviderBranding(
|
||||
label: {
|
||||
'github': 'GitHub',
|
||||
'gitlab': 'GitLab',
|
||||
}[op.toLowerCase()] ??
|
||||
toCapitalized(op),
|
||||
iconKey: op.toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconOP extends StatelessWidget {
|
||||
final String op;
|
||||
final String? icon;
|
||||
@@ -74,11 +103,8 @@ class ButtonOP extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final opLabel = {
|
||||
'github': 'GitHub',
|
||||
'gitlab': 'GitLab'
|
||||
}[op.toLowerCase()] ??
|
||||
toCapitalized(op);
|
||||
final branding = _oidcProviderBranding(op);
|
||||
final buttonLabel = translate("Continue with {${branding.label}}");
|
||||
return Row(children: [
|
||||
Container(
|
||||
height: height,
|
||||
@@ -95,7 +121,7 @@ class ButtonOP extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: _IconOP(
|
||||
op: op,
|
||||
op: branding.iconKey,
|
||||
icon: icon,
|
||||
margin: EdgeInsets.only(right: 5),
|
||||
),
|
||||
@@ -103,8 +129,7 @@ class ButtonOP extends StatelessWidget {
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Text(translate("Continue with {$opLabel}"))),
|
||||
child: Center(child: Text(buttonLabel)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
// Official
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
), (instance) {
|
||||
() => TapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onTapDown = onTapDown
|
||||
..onTapUp = onTapUp
|
||||
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
|
||||
}),
|
||||
DoubleTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||
() => DoubleTapGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
), (instance) {
|
||||
() => DoubleTapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onDoubleTapDown = onDoubleTapDown
|
||||
..onDoubleTap = onDoubleTap;
|
||||
}),
|
||||
LongPressGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
), (instance) {
|
||||
() => LongPressGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onLongPressDown = onLongPressDown
|
||||
..onLongPressUp = onLongPressUp
|
||||
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
// Customized
|
||||
HoldTapMoveGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||
() => HoldTapMoveGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
),
|
||||
() => HoldTapMoveGestureRecognizer(),
|
||||
(instance) => instance
|
||||
..onHoldDragStart = onHoldDragStart
|
||||
..onHoldDragUpdate = onHoldDragUpdate
|
||||
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
|
||||
..onHoldDragEnd = onHoldDragEnd),
|
||||
DoubleFinerTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
||||
() => DoubleFinerTapGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
), (instance) {
|
||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onDoubleFinerTap = onDoubleFinerTap
|
||||
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
||||
}),
|
||||
CustomTouchGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||
() => CustomTouchGestureRecognizer(
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
), (instance) {
|
||||
() => CustomTouchGestureRecognizer(), (instance) {
|
||||
instance.onOneFingerPanStart =
|
||||
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
||||
instance
|
||||
|
||||
@@ -398,6 +398,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
.contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
_allPeersLoader.queryOnlines(_autocompleteOpts);
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
|
||||
@@ -65,7 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
late final TextEditingController controller;
|
||||
final RxBool startmenu = true.obs;
|
||||
final RxBool desktopicon = true.obs;
|
||||
final RxBool printer = true.obs;
|
||||
final RxBool printer = false.obs;
|
||||
final RxBool showProgress = false.obs;
|
||||
final RxBool btnEnabled = true.obs;
|
||||
|
||||
@@ -80,7 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||
printer.value = installOptions['PRINTER'] != '0';
|
||||
printer.value = installOptions['PRINTER'] == '1';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -207,6 +207,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
.contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
_allPeersLoader.queryOnlines(_autocompleteOpts);
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
|
||||
@@ -517,10 +517,12 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
}
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
),
|
||||
child: inputModel.isPhysicalMouse.value
|
||||
? getBodyForMobile()
|
||||
: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
}
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
isCamera: true,
|
||||
),
|
||||
child: inputModel.isPhysicalMouse.value
|
||||
? getBodyForMobile()
|
||||
: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
isCamera: true,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -117,13 +117,13 @@ void showServerSettingsWithValue(
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
child: serverSettingsTextFormField(
|
||||
label: label,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
),
|
||||
errorMsg: errorMsg,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
showLabelText: false,
|
||||
validator: validator,
|
||||
autofocus: autofocus,
|
||||
).workaroundFreezeLinuxMint(),
|
||||
@@ -132,12 +132,10 @@ void showServerSettingsWithValue(
|
||||
);
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
return serverSettingsTextFormField(
|
||||
label: label,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||
),
|
||||
errorMsg: errorMsg,
|
||||
validator: validator,
|
||||
).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(
|
||||
OverlayDialogManager dialogManager,
|
||||
List<TToggleMenu> privacyModeList,
|
||||
|
||||
@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
||||
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
||||
final _constSessionId = Uuid().v4obj();
|
||||
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
|
||||
const _restartReconnectSilentDelaySecs = 5;
|
||||
|
||||
class CachedPeerData {
|
||||
Map<String, dynamic> updatePrivacyMode = {};
|
||||
@@ -119,6 +121,7 @@ class FfiModel with ChangeNotifier {
|
||||
bool _touchMode = false;
|
||||
late VirtualMouseMode virtualMouseMode;
|
||||
Timer? _timer;
|
||||
Timer? _restartReconnectDelayTimer;
|
||||
var _reconnects = 1;
|
||||
DateTime? _offlineReconnectStartTime;
|
||||
bool _viewOnly = false;
|
||||
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
|
||||
_inputBlocked = false;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
resetRestartReconnectState();
|
||||
clearPermissions();
|
||||
waitForImageTimer?.cancel();
|
||||
timerScreenshot?.cancel();
|
||||
@@ -341,6 +345,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'connection_ready') {
|
||||
setConnectionType(peerId, evt['secure'] == 'true',
|
||||
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
||||
resetRestartReconnectState();
|
||||
} else if (name == 'switch_display') {
|
||||
// switch display is kept for backward compatibility
|
||||
handleSwitchDisplay(evt, sessionId, peerId);
|
||||
@@ -922,8 +927,28 @@ class FfiModel with ChangeNotifier {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||
} else if (type == 'restarting') {
|
||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
||||
hasCancel: false);
|
||||
// Treat restart messages as reconnect control events. Rust still sends
|
||||
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||
// frame briefly, then shows the Connecting overlay.
|
||||
if (_restartReconnectDelayTimer == null) {
|
||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
|
||||
clearPermissions();
|
||||
// Retry once more after the silent window so restart reconnect attempts
|
||||
// are spaced by the empirical short cadence instead of only updating UI.
|
||||
_restartReconnectDelayTimer =
|
||||
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
|
||||
_restartReconnectDelayTimer = null;
|
||||
if (parent.target?.closed == true) {
|
||||
return;
|
||||
}
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
});
|
||||
}
|
||||
} else if (type == 'restarting-show') {
|
||||
_restartReconnectDelayTimer?.cancel();
|
||||
_restartReconnectDelayTimer = null;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
} else if (type == 'wait-remote-accept-nook') {
|
||||
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
||||
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||
@@ -949,6 +974,11 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void resetRestartReconnectState() {
|
||||
_restartReconnectDelayTimer?.cancel();
|
||||
_restartReconnectDelayTimer = null;
|
||||
}
|
||||
|
||||
/// Auto-retry check for "Remote desktop is offline" error.
|
||||
/// returns true to auto-retry, false otherwise.
|
||||
bool shouldAutoRetryOnOffline(
|
||||
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
resetRestartReconnectState();
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = false;
|
||||
}
|
||||
@@ -3666,6 +3697,7 @@ class FFI {
|
||||
|
||||
/// Mobile reuse FFI
|
||||
void mobileReset() {
|
||||
ffiModel.resetRestartReconnectState();
|
||||
ffiModel.waitForFirstImage.value = true;
|
||||
ffiModel.isRefreshing = false;
|
||||
ffiModel.waitForImageDialogShow.value = true;
|
||||
@@ -3879,6 +3911,7 @@ class FFI {
|
||||
}
|
||||
if (ffiModel.waitForFirstImage.value == true) {
|
||||
ffiModel.waitForFirstImage.value = false;
|
||||
ffiModel.resetRestartReconnectState();
|
||||
dialogManager.dismissAll();
|
||||
await canvasModel.updateViewStyle();
|
||||
await canvasModel.updateScrollStyle();
|
||||
|
||||
@@ -145,23 +145,26 @@ class Peer {
|
||||
note == other.note;
|
||||
}
|
||||
|
||||
Peer.copy(Peer other)
|
||||
: this(
|
||||
id: other.id,
|
||||
hash: other.hash,
|
||||
password: other.password,
|
||||
username: other.username,
|
||||
hostname: other.hostname,
|
||||
platform: other.platform,
|
||||
alias: other.alias,
|
||||
tags: other.tags.toList(),
|
||||
forceAlwaysRelay: other.forceAlwaysRelay,
|
||||
rdpPort: other.rdpPort,
|
||||
rdpUsername: other.rdpUsername,
|
||||
loginName: other.loginName,
|
||||
device_group_name: other.device_group_name,
|
||||
note: other.note,
|
||||
sameServer: other.sameServer);
|
||||
factory Peer.copy(Peer other) {
|
||||
final peer = Peer(
|
||||
id: other.id,
|
||||
hash: other.hash,
|
||||
password: other.password,
|
||||
username: other.username,
|
||||
hostname: other.hostname,
|
||||
platform: other.platform,
|
||||
alias: other.alias,
|
||||
tags: other.tags.toList(),
|
||||
forceAlwaysRelay: other.forceAlwaysRelay,
|
||||
rdpPort: other.rdpPort,
|
||||
rdpUsername: other.rdpUsername,
|
||||
loginName: other.loginName,
|
||||
device_group_name: other.device_group_name,
|
||||
note: other.note,
|
||||
sameServer: other.sameServer);
|
||||
peer.online = other.online;
|
||||
return peer;
|
||||
}
|
||||
}
|
||||
|
||||
enum UpdateEvent { online, load }
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# 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
|
||||
version: 1.4.7+65
|
||||
version: 1.4.8+66
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
148
flutter/test/autocomplete_peer_merge_test.dart
Normal file
148
flutter/test/autocomplete_peer_merge_test.dart
Normal 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
63
flutter/test/cm_demo.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -1,62 +1,20 @@
|
||||
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';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
final testClients = [
|
||||
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)
|
||||
];
|
||||
import 'cm_demo.dart' as cm_demo;
|
||||
|
||||
/// flutter run -d {platform} -t test/cm_test.dart to test cm
|
||||
void main(List<String> args) 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)
|
||||
void main() {
|
||||
test('connection manager demo clients match the current Client API', () {
|
||||
expect(cm_demo.testClients, hasLength(4));
|
||||
expect(cm_demo.testClients.map((client) => client.name), [
|
||||
'UserAAAAAA',
|
||||
'UserBBBBB',
|
||||
'UserC',
|
||||
'UserDDDDDDDDDDDd',
|
||||
]);
|
||||
// ensure
|
||||
windowManager.setAlignment(Alignment.topRight);
|
||||
expect(
|
||||
cm_demo.testClients.every(
|
||||
(client) => client.keyboard && !client.clipboard && !client.audio),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
38
flutter/test/server_settings_dialog_test.dart
Normal file
38
flutter/test/server_settings_dialog_test.dart
Normal 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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,8 @@ use fuser::MountOption;
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -53,8 +54,16 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
if let Some(ctx) = fuse_context_lock.as_ref() {
|
||||
if is_mount_point_healthy(&ctx.mount_point) {
|
||||
return Ok(());
|
||||
}
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is disconnected, remounting",
|
||||
ctx.mount_point.display()
|
||||
);
|
||||
let stale_context = fuse_context_lock.take();
|
||||
drop(stale_context);
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
@@ -159,35 +168,100 @@ struct FuseContext {
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
fn prepare_fuse_mount_point(mount_point: &Path) {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
if let Some(parent) = mount_point.parent() {
|
||||
if let Err(e) = fs::create_dir_all(parent) {
|
||||
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
unmount_fuse_mount_point(mount_point);
|
||||
|
||||
if let Err(e) = fs::create_dir_all(mount_point) {
|
||||
log::warn!(
|
||||
"failed to create FUSE mount point {:?}: {:?}",
|
||||
mount_point,
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
|
||||
log::warn!(
|
||||
"failed to set FUSE mount point permissions {:?}: {:?}",
|
||||
mount_point,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
fn is_mount_point_healthy(mount_point: &Path) -> bool {
|
||||
is_mount_point_healthy_result(std::fs::metadata(mount_point))
|
||||
}
|
||||
|
||||
fn is_mount_point_healthy_result<T>(result: io::Result<T>) -> bool {
|
||||
match result {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
e.raw_os_error() != Some(libc::ENOTCONN) && e.kind() != io::ErrorKind::NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unmount_fuse_mount_point(mount_point: &Path) {
|
||||
if run_unmount_command("umount", &["-l"], mount_point) {
|
||||
return;
|
||||
}
|
||||
if run_unmount_command("fusermount3", &["-uz"], mount_point) {
|
||||
return;
|
||||
}
|
||||
run_unmount_command("fusermount", &["-uz"], mount_point);
|
||||
}
|
||||
|
||||
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
|
||||
match std::process::Command::new(program)
|
||||
.args(args)
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => {
|
||||
log::debug!(
|
||||
"{} {:?} exited with status {:?}",
|
||||
program,
|
||||
mount_point,
|
||||
status.code()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
let ctx = fuse_context_lock.take();
|
||||
drop(ctx);
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
unmount_fuse_mount_point(&self.mount_point);
|
||||
if let Some(session) = self.session.lock().take() {
|
||||
session.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,3 +297,30 @@ impl FuseContext {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{fs, io};
|
||||
|
||||
#[test]
|
||||
fn reports_disconnected_fuse_mount_as_unhealthy() {
|
||||
let err = io::Error::from_raw_os_error(libc::ENOTCONN);
|
||||
|
||||
assert!(!is_mount_point_healthy_result::<()>(Err(err)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_existing_directory_mount_point_as_healthy() {
|
||||
let mount_point = std::env::temp_dir().join(format!(
|
||||
"rustdesk-fuse-mount-health-test-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&mount_point);
|
||||
fs::create_dir(&mount_point).unwrap();
|
||||
|
||||
assert!(is_mount_point_healthy(&mount_point));
|
||||
|
||||
let _ = fs::remove_dir_all(&mount_point);
|
||||
}
|
||||
}
|
||||
|
||||
Submodule libs/hbb_common updated: df6badca5b...387603f47c
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.7"
|
||||
version = "1.4.8"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import optparse
|
||||
import subprocess
|
||||
from hashlib import md5
|
||||
import brotli
|
||||
import datetime
|
||||
@@ -65,11 +66,15 @@ def write_app_metadata(output_folder: str):
|
||||
print(f"App metadata has been written to {output_path}")
|
||||
|
||||
def build_portable(output_folder: str, target: str):
|
||||
os.chdir(output_folder)
|
||||
if target:
|
||||
os.system("cargo build --locked --release --target " + target)
|
||||
else:
|
||||
os.system("cargo build --locked --release")
|
||||
current_dir = os.getcwd()
|
||||
try:
|
||||
os.chdir(output_folder)
|
||||
cmd = ["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
|
||||
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.7
|
||||
pkgver=1.4.8
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
||||
sed -i "s/\b$1\b/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
||||
cargo run # to bump version in cargo lock
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
|
||||
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
|
||||
<!-- Remote printer also works on Win8.1 in my test. -->
|
||||
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT >= 603 AND PRINTER = 1 OR PRINTER = "Y" OR PRINTER = "y"" />
|
||||
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT >= 603 AND (PRINTER = 1 OR PRINTER = "Y" OR PRINTER = "y")" />
|
||||
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT >= 603" />
|
||||
|
||||
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
<?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
|
||||
install start menu shortcuts, they are initialized with a default value to install shortcuts.
|
||||
They should not be set directly from the command line or registry, instead the CREATE* properties
|
||||
below should be set, then they will update these properties with their values only if set. -->
|
||||
install start menu shortcuts or the printer. Shortcut properties default to install;
|
||||
PRINTER defaults to not install. The CREATE* properties below update shortcut
|
||||
properties from command line, bundle, or registry values. -->
|
||||
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
|
||||
<Property Id="DESKTOPSHORTCUTS" 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,
|
||||
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 -->
|
||||
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = "Y" OR CREATESTARTMENUSHORTCUTS = "y")" />
|
||||
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = "Y" OR CREATEDESKTOPSHORTCUTS = "y")" />
|
||||
<SetProperty Id="PRINTER" Value="" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = "Y" OR INSTALLPRINTER = "y")" />
|
||||
<!-- 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 = "Y" OR INSTALLPRINTER = "y"" />
|
||||
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = "Y" OR INSTALLPRINTER = "y")" />
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.7
|
||||
Version: 1.4.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.7
|
||||
Version: 1.4.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.7
|
||||
Version: 1.4.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -96,6 +96,8 @@ pub mod screenshot;
|
||||
|
||||
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||
pub const SEC30: Duration = Duration::from_secs(30);
|
||||
// Empirical restart reconnect grace window.
|
||||
const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
|
||||
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
||||
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
||||
|
||||
@@ -1740,7 +1742,10 @@ pub struct LoginConfigHandler {
|
||||
features: Option<Features>,
|
||||
pub session_id: u64, // used for local <-> server communication
|
||||
pub supported_encoding: SupportedEncoding,
|
||||
pub restarting_remote_device: bool,
|
||||
restarting_remote_device: bool,
|
||||
// Start time of the restart grace window. On Windows the peer may briefly
|
||||
// reconnect before the real reboot disconnect.
|
||||
restart_remote_device_at: Option<Instant>,
|
||||
pub force_relay: bool,
|
||||
pub direct: Option<bool>,
|
||||
pub received: bool,
|
||||
@@ -1849,7 +1854,7 @@ impl LoginConfigHandler {
|
||||
}
|
||||
self.session_id = sid;
|
||||
self.supported_encoding = Default::default();
|
||||
self.restarting_remote_device = false;
|
||||
self.clear_restarting_remote_device();
|
||||
self.force_relay =
|
||||
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
||||
|| force_relay
|
||||
@@ -2779,6 +2784,30 @@ impl LoginConfigHandler {
|
||||
msg_out
|
||||
}
|
||||
|
||||
pub fn mark_restarting_remote_device(&mut self) {
|
||||
self.restarting_remote_device = true;
|
||||
self.restart_remote_device_at = Some(Instant::now());
|
||||
}
|
||||
|
||||
pub fn clear_restarting_remote_device(&mut self) {
|
||||
self.restarting_remote_device = false;
|
||||
self.restart_remote_device_at = None;
|
||||
}
|
||||
|
||||
pub fn is_restarting_remote_device(&self) -> bool {
|
||||
if !self.restarting_remote_device {
|
||||
return false;
|
||||
}
|
||||
// Keep this flag alive for a short grace window instead of clearing it on
|
||||
// connection_ready or the first peer bytes. During OS restart the peer can
|
||||
// briefly reconnect before the real reboot disconnect, and clearing it too
|
||||
// early would let the next disconnect escape the restart flow and fall back
|
||||
// to the normal error dialog / manual reconnect path.
|
||||
self.restart_remote_device_at
|
||||
.map(|started_at| started_at.elapsed() < RESTART_REMOTE_DEVICE_GRACE)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_conn_token(&self) -> Option<String> {
|
||||
if self.password.is_empty() {
|
||||
return None;
|
||||
@@ -3718,9 +3747,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
fn on_establish_connection_error(&self, err: String) {
|
||||
let title = "Connection Error";
|
||||
let text = err.to_string();
|
||||
let lc = self.get_lch();
|
||||
let direct = lc.read().unwrap().direct;
|
||||
let received = lc.read().unwrap().received;
|
||||
let lch = self.get_lch();
|
||||
let (is_restarting, direct, received) = {
|
||||
let lc = lch.read().unwrap();
|
||||
(lc.is_restarting_remote_device(), lc.direct, lc.received)
|
||||
};
|
||||
if is_restarting {
|
||||
log::info!("Restart remote device, suppress connection error: {err}");
|
||||
// Flutter treats this as a reconnect control event. The text is kept
|
||||
// for legacy UI and existing translation reuse.
|
||||
self.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut relay_hint = false;
|
||||
let mut relay_hint_type = "relay-hint";
|
||||
|
||||
@@ -10,6 +10,10 @@ use crate::{
|
||||
common::get_default_sound_input,
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
|
||||
// Empirical no-data window before exposing the restart reconnect state to the UI.
|
||||
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
|
||||
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||
#[cfg(any(
|
||||
@@ -153,7 +157,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_recv_time = Instant::now();
|
||||
let mut received = false;
|
||||
let conn_type = if self.handler.is_file_transfer() {
|
||||
ConnType::FILE_TRANSFER
|
||||
@@ -219,6 +222,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let mut fps_instant = Instant::now();
|
||||
|
||||
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
||||
let mut last_recv_time = Instant::now();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -244,7 +248,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
} else {
|
||||
if self.handler.is_restarting_remote_device() {
|
||||
log::info!("Restart remote device");
|
||||
self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", "");
|
||||
self.handler.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||
} else {
|
||||
log::info!("Reset by the peer");
|
||||
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
|
||||
@@ -279,6 +283,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
_ = status_timer.tick() => {
|
||||
if self.handler.is_restarting_remote_device()
|
||||
&& last_recv_time.elapsed() >= RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT
|
||||
{
|
||||
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||
break;
|
||||
}
|
||||
let elapsed = fps_instant.elapsed().as_millis();
|
||||
if elapsed < 1000 {
|
||||
continue;
|
||||
@@ -1426,7 +1436,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.handler.set_cursor_position(cp);
|
||||
}
|
||||
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")))]
|
||||
update_clipboard(vec![cb], ClipboardSide::Client);
|
||||
#[cfg(target_os = "ios")]
|
||||
@@ -1445,7 +1459,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
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")))]
|
||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
||||
#[cfg(target_os = "ios")]
|
||||
|
||||
@@ -262,11 +262,9 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
if config::is_disable_installation() {
|
||||
return None;
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
let options = "desktopicon startmenu";
|
||||
#[cfg(windows)]
|
||||
let options = "desktopicon startmenu printer";
|
||||
let res = platform::install_me(options, "".to_owned(), true, args.len() > 1);
|
||||
let (printer_override, debug) = parse_silent_install_args(&args);
|
||||
let options = platform::get_silent_install_options(printer_override);
|
||||
let res = platform::install_me(options, "".to_owned(), true, debug);
|
||||
let text = match res {
|
||||
Ok(_) => translate("Installation Successful!".to_string()),
|
||||
Err(err) => {
|
||||
@@ -933,6 +931,23 @@ fn is_cli_setting_change_disabled() -> bool {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -326,12 +326,14 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
|
||||
try_sync_peer_option(&session, &session_id, &value, None);
|
||||
}
|
||||
#[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();
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Refresh random password", "Atualizar senha aleatória"),
|
||||
("Set your own password", "Configure sua própria senha"),
|
||||
("Enable keyboard/mouse", "Habilitar teclado/mouse"),
|
||||
("Enable clipboard", "Habilitar Área de Transferência"),
|
||||
("Enable file transfer", "Habilitar Transferência de Arquivos"),
|
||||
("Enable TCP tunneling", "Habilitar Tunelamento TCP"),
|
||||
("Enable clipboard", "Habilitar área de transferência"),
|
||||
("Enable file transfer", "Habilitar transferência de arquivos"),
|
||||
("Enable TCP tunneling", "Habilitar tunelamento TCP"),
|
||||
("IP Whitelisting", "Lista de IPs Confiáveis"),
|
||||
("ID/Relay Server", "Servidor ID/Relay"),
|
||||
("Import server config", "Importar Configuração do Servidor"),
|
||||
@@ -430,7 +430,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Strong", "Forte"),
|
||||
("Switch Sides", "Trocar de lado"),
|
||||
("Please confirm if you want to share your desktop?", "Por favor, confirme se você deseja compartilhar sua área de trabalho?"),
|
||||
("Display", "Display"),
|
||||
("Display", "Exibição"),
|
||||
("Default View Style", "Estilo de Visualização Padrão"),
|
||||
("Default Scroll Style", "Estilo de Rolagem Padrão"),
|
||||
("Default Image Quality", "Qualidade de Imagem Padrão"),
|
||||
@@ -693,11 +693,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
|
||||
("Enable UDP hole punching", "Habilitar UDP hole punching"),
|
||||
("View camera", "Visualizar câmera"),
|
||||
("Enable camera", "Ativar câmera"),
|
||||
("No cameras", "Sem câmeras"),
|
||||
("Enable camera", "Habilitar câmera"),
|
||||
("No cameras", "Nenhuma câmeras"),
|
||||
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
|
||||
("Terminal", "Terminal"),
|
||||
("Enable terminal", "Habilitar Terminal"),
|
||||
("Enable terminal", "Habilitar terminal"),
|
||||
("New tab", "Nova aba"),
|
||||
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
|
||||
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
|
||||
@@ -744,7 +744,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("password-hidden-tip", "A senha permanente está definida como (oculta)."),
|
||||
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
|
||||
("Enable privacy mode", "Habilitar modo de privacidade"),
|
||||
("allow-remote-toolbar-docking-any-edge", "Permitir fixar a barra de ferramentas remota em qualquer borda da janela"),
|
||||
("allow-remote-toolbar-docking-any-edge", "Fixar a barra de ferramentas remota em qualquer borda da janela"),
|
||||
("API Token", "Token de API"),
|
||||
("Deploy", "Implantar"),
|
||||
("Custom ID (optional)", "ID personalizado (opcional)"),
|
||||
|
||||
@@ -1324,6 +1324,23 @@ pub fn get_install_options() -> String {
|
||||
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.
|
||||
fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option<String> {
|
||||
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT);
|
||||
|
||||
@@ -30,7 +30,6 @@ fn run_rdp(port: u16) {
|
||||
if !password.is_empty() {
|
||||
args.push(format!("/pass:{}", password));
|
||||
}
|
||||
println!("{:?}", args);
|
||||
std::process::Command::new("cmdkey")
|
||||
.args(&args)
|
||||
.output()
|
||||
|
||||
@@ -2891,7 +2891,7 @@ impl Connection {
|
||||
self.update_auto_disconnect_timer();
|
||||
}
|
||||
Some(message::Union::Clipboard(cb)) => {
|
||||
if self.clipboard {
|
||||
if self.clipboard_enabled() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
update_clipboard(vec![cb], ClipboardSide::Host);
|
||||
// ios as the controlled side is actually not supported for now.
|
||||
@@ -2919,12 +2919,12 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
Some(message::Union::MultiClipboards(_mcb)) => {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.clipboard {
|
||||
if self.clipboard_enabled() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
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"))]
|
||||
Some(message::Union::Cliprdr(clip)) => {
|
||||
|
||||
@@ -175,13 +175,16 @@ impl SessionPermissionConfig {
|
||||
*self.server_clipboard_enabled.read().unwrap()
|
||||
&& *self.server_keyboard_enabled.read().unwrap()
|
||||
&& !self.lc.read().unwrap().disable_clipboard.v
|
||||
&& !self.lc.read().unwrap().view_only.v
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn is_file_clipboard_required(&self) -> bool {
|
||||
let lc = self.lc.read().unwrap();
|
||||
*self.server_keyboard_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_keyboard_enabled.read().unwrap()
|
||||
&& !self.lc.read().unwrap().disable_clipboard.v
|
||||
&& !self.lc.read().unwrap().view_only.v
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
pub fn is_file_clipboard_required(&self) -> bool {
|
||||
let lc = self.lc.read().unwrap();
|
||||
*self.server_keyboard_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")]
|
||||
@@ -554,7 +560,7 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
|
||||
pub fn restart_remote_device(&self) {
|
||||
let mut lc = self.lc.write().unwrap();
|
||||
lc.restarting_remote_device = true;
|
||||
lc.mark_restarting_remote_device();
|
||||
let msg = lc.restart_remote_device();
|
||||
self.send(Data::Message(msg));
|
||||
}
|
||||
@@ -650,7 +656,7 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
}
|
||||
|
||||
pub fn is_restarting_remote_device(&self) -> bool {
|
||||
self.lc.read().unwrap().restarting_remote_device
|
||||
self.lc.read().unwrap().is_restarting_remote_device()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
Reference in New Issue
Block a user