mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-06 15:25:09 +03:00
Compare commits
10 Commits
screenlock
...
fuse-file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0625126e | ||
|
|
8a955888bf | ||
|
|
36e812e550 | ||
|
|
8baa995c7a | ||
|
|
f4a0535289 | ||
|
|
6665242edf | ||
|
|
3cdf1cce54 | ||
|
|
88ae00ba73 | ||
|
|
7c26575dbd | ||
|
|
93d064a9b0 |
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`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
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
|
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"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
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"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
VERSION: "1.4.7"
|
VERSION: "1.4.8"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -3952,7 +3952,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738"
|
source = "git+https://github.com/rustdesk-org/hwcodec#778df1f99597722473b29443bac22ae6c23946fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -7270,7 +7270,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7385,7 +7385,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.7
|
version: 1.4.8
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.7
|
version: 1.4.8
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
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 |
@@ -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 {
|
||||||
|
|||||||
@@ -24,6 +24,35 @@ const kOpSvgList = [
|
|||||||
'microsoft'
|
'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 {
|
class _IconOP extends StatelessWidget {
|
||||||
final String op;
|
final String op;
|
||||||
final String? icon;
|
final String? icon;
|
||||||
@@ -74,11 +103,8 @@ class ButtonOP extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final opLabel = {
|
final branding = _oidcProviderBranding(op);
|
||||||
'github': 'GitHub',
|
final buttonLabel = translate("Continue with {${branding.label}}");
|
||||||
'gitlab': 'GitLab'
|
|
||||||
}[op.toLowerCase()] ??
|
|
||||||
toCapitalized(op);
|
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
Container(
|
Container(
|
||||||
height: height,
|
height: height,
|
||||||
@@ -95,7 +121,7 @@ class ButtonOP extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 30,
|
width: 30,
|
||||||
child: _IconOP(
|
child: _IconOP(
|
||||||
op: op,
|
op: branding.iconKey,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
margin: EdgeInsets.only(right: 5),
|
margin: EdgeInsets.only(right: 5),
|
||||||
),
|
),
|
||||||
@@ -103,8 +129,7 @@ class ButtonOP extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Center(
|
child: Center(child: Text(buttonLabel)),
|
||||||
child: Text(translate("Continue with {$opLabel}"))),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
|
|||||||
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
||||||
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
||||||
final _constSessionId = Uuid().v4obj();
|
final _constSessionId = Uuid().v4obj();
|
||||||
|
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
|
||||||
|
const _restartReconnectSilentDelaySecs = 5;
|
||||||
|
|
||||||
class CachedPeerData {
|
class CachedPeerData {
|
||||||
Map<String, dynamic> updatePrivacyMode = {};
|
Map<String, dynamic> updatePrivacyMode = {};
|
||||||
@@ -119,6 +121,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
bool _touchMode = false;
|
bool _touchMode = false;
|
||||||
late VirtualMouseMode virtualMouseMode;
|
late VirtualMouseMode virtualMouseMode;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
timerScreenshot?.cancel();
|
timerScreenshot?.cancel();
|
||||||
@@ -341,6 +345,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
setConnectionType(peerId, evt['secure'] == 'true',
|
setConnectionType(peerId, evt['secure'] == 'true',
|
||||||
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
||||||
|
resetRestartReconnectState();
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
// switch display is kept for backward compatibility
|
// switch display is kept for backward compatibility
|
||||||
handleSwitchDisplay(evt, sessionId, peerId);
|
handleSwitchDisplay(evt, sessionId, peerId);
|
||||||
@@ -922,8 +927,28 @@ class FfiModel with ChangeNotifier {
|
|||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
hasCancel: false);
|
// 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') {
|
} else if (type == 'wait-remote-accept-nook') {
|
||||||
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
} 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.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
_offlineReconnectStartTime = null;
|
_offlineReconnectStartTime = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
@@ -3666,6 +3697,7 @@ class FFI {
|
|||||||
|
|
||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
ffiModel.isRefreshing = false;
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
@@ -3879,6 +3911,7 @@ class FFI {
|
|||||||
}
|
}
|
||||||
if (ffiModel.waitForFirstImage.value == true) {
|
if (ffiModel.waitForFirstImage.value == true) {
|
||||||
ffiModel.waitForFirstImage.value = false;
|
ffiModel.waitForFirstImage.value = false;
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
await canvasModel.updateViewStyle();
|
await canvasModel.updateViewStyle();
|
||||||
await canvasModel.updateScrollStyle();
|
await canvasModel.updateScrollStyle();
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.4.7+65
|
version: 1.4.8+66
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
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_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,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
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 hbb_common::{config::APP_NAME, log};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
sync::{mpsc::Sender, Arc},
|
sync::{mpsc::Sender, Arc},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -53,8 +54,16 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
|||||||
} else {
|
} else {
|
||||||
FUSE_CONTEXT_SERVER.lock()
|
FUSE_CONTEXT_SERVER.lock()
|
||||||
};
|
};
|
||||||
if fuse_context_lock.is_some() {
|
if let Some(ctx) = fuse_context_lock.as_ref() {
|
||||||
return Ok(());
|
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 {
|
let mount_point = if is_client {
|
||||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||||
@@ -159,35 +168,100 @@ struct FuseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function must be called after the main IPC is up
|
// 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::{
|
use std::{
|
||||||
fs::{self, Permissions},
|
fs::{self, Permissions},
|
||||||
os::unix::prelude::PermissionsExt,
|
os::unix::prelude::PermissionsExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::create_dir(mount_point).ok();
|
if let Some(parent) = mount_point.parent() {
|
||||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
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")
|
unmount_fuse_mount_point(mount_point);
|
||||||
.arg(mount_point)
|
|
||||||
.status()
|
if let Err(e) = fs::create_dir_all(mount_point) {
|
||||||
{
|
log::warn!(
|
||||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
"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) {
|
fn is_mount_point_healthy(mount_point: &Path) -> bool {
|
||||||
if is_client {
|
is_mount_point_healthy_result(std::fs::metadata(mount_point))
|
||||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
}
|
||||||
} else {
|
|
||||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
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 {
|
impl Drop for FuseContext {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.session.lock().take().map(|s| s.join());
|
|
||||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
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())
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.4.7
|
pkgver=1.4.8
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#! /usr/bin/env bash
|
#! /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
|
cargo run # to bump version in cargo lock
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.8
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.8
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.8
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ pub mod screenshot;
|
|||||||
|
|
||||||
pub const MILLI1: Duration = Duration::from_millis(1);
|
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||||
pub const SEC30: Duration = Duration::from_secs(30);
|
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;
|
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
||||||
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
||||||
|
|
||||||
@@ -1740,7 +1742,10 @@ pub struct LoginConfigHandler {
|
|||||||
features: Option<Features>,
|
features: Option<Features>,
|
||||||
pub session_id: u64, // used for local <-> server communication
|
pub session_id: u64, // used for local <-> server communication
|
||||||
pub supported_encoding: SupportedEncoding,
|
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 force_relay: bool,
|
||||||
pub direct: Option<bool>,
|
pub direct: Option<bool>,
|
||||||
pub received: bool,
|
pub received: bool,
|
||||||
@@ -1849,7 +1854,7 @@ impl LoginConfigHandler {
|
|||||||
}
|
}
|
||||||
self.session_id = sid;
|
self.session_id = sid;
|
||||||
self.supported_encoding = Default::default();
|
self.supported_encoding = Default::default();
|
||||||
self.restarting_remote_device = false;
|
self.clear_restarting_remote_device();
|
||||||
self.force_relay =
|
self.force_relay =
|
||||||
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
||||||
|| force_relay
|
|| force_relay
|
||||||
@@ -2779,6 +2784,30 @@ impl LoginConfigHandler {
|
|||||||
msg_out
|
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> {
|
pub fn get_conn_token(&self) -> Option<String> {
|
||||||
if self.password.is_empty() {
|
if self.password.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -3718,9 +3747,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
fn on_establish_connection_error(&self, err: String) {
|
fn on_establish_connection_error(&self, err: String) {
|
||||||
let title = "Connection Error";
|
let title = "Connection Error";
|
||||||
let text = err.to_string();
|
let text = err.to_string();
|
||||||
let lc = self.get_lch();
|
let lch = self.get_lch();
|
||||||
let direct = lc.read().unwrap().direct;
|
let (is_restarting, direct, received) = {
|
||||||
let received = lc.read().unwrap().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 = false;
|
||||||
let mut relay_hint_type = "relay-hint";
|
let mut relay_hint_type = "relay-hint";
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ use crate::{
|
|||||||
common::get_default_sound_input,
|
common::get_default_sound_input,
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
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")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
@@ -153,7 +157,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut last_recv_time = Instant::now();
|
|
||||||
let mut received = false;
|
let mut received = false;
|
||||||
let conn_type = if self.handler.is_file_transfer() {
|
let conn_type = if self.handler.is_file_transfer() {
|
||||||
ConnType::FILE_TRANSFER
|
ConnType::FILE_TRANSFER
|
||||||
@@ -219,6 +222,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
let mut fps_instant = Instant::now();
|
let mut fps_instant = Instant::now();
|
||||||
|
|
||||||
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
||||||
|
let mut last_recv_time = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -244,7 +248,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
} else {
|
} else {
|
||||||
if self.handler.is_restarting_remote_device() {
|
if self.handler.is_restarting_remote_device() {
|
||||||
log::info!("Restart 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 {
|
} else {
|
||||||
log::info!("Reset by the peer");
|
log::info!("Reset by the peer");
|
||||||
self.handler.msgbox("error", "Connection Error", "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() => {
|
_ = 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();
|
let elapsed = fps_instant.elapsed().as_millis();
|
||||||
if elapsed < 1000 {
|
if elapsed < 1000 {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Refresh random password", "Atualizar senha aleatória"),
|
("Refresh random password", "Atualizar senha aleatória"),
|
||||||
("Set your own password", "Configure sua própria senha"),
|
("Set your own password", "Configure sua própria senha"),
|
||||||
("Enable keyboard/mouse", "Habilitar teclado/mouse"),
|
("Enable keyboard/mouse", "Habilitar teclado/mouse"),
|
||||||
("Enable clipboard", "Habilitar Área de Transferência"),
|
("Enable clipboard", "Habilitar área de transferência"),
|
||||||
("Enable file transfer", "Habilitar Transferência de Arquivos"),
|
("Enable file transfer", "Habilitar transferência de arquivos"),
|
||||||
("Enable TCP tunneling", "Habilitar Tunelamento TCP"),
|
("Enable TCP tunneling", "Habilitar tunelamento TCP"),
|
||||||
("IP Whitelisting", "Lista de IPs Confiáveis"),
|
("IP Whitelisting", "Lista de IPs Confiáveis"),
|
||||||
("ID/Relay Server", "Servidor ID/Relay"),
|
("ID/Relay Server", "Servidor ID/Relay"),
|
||||||
("Import server config", "Importar Configuração do Servidor"),
|
("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"),
|
("Strong", "Forte"),
|
||||||
("Switch Sides", "Trocar de lado"),
|
("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?"),
|
("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 View Style", "Estilo de Visualização Padrão"),
|
||||||
("Default Scroll Style", "Estilo de Rolagem Padrão"),
|
("Default Scroll Style", "Estilo de Rolagem Padrão"),
|
||||||
("Default Image Quality", "Qualidade de Imagem 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 IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
|
||||||
("Enable UDP hole punching", "Habilitar UDP hole punching"),
|
("Enable UDP hole punching", "Habilitar UDP hole punching"),
|
||||||
("View camera", "Visualizar câmera"),
|
("View camera", "Visualizar câmera"),
|
||||||
("Enable camera", "Ativar câmera"),
|
("Enable camera", "Habilitar câmera"),
|
||||||
("No cameras", "Sem câmeras"),
|
("No cameras", "Nenhuma câmeras"),
|
||||||
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
|
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
|
||||||
("Terminal", "Terminal"),
|
("Terminal", "Terminal"),
|
||||||
("Enable terminal", "Habilitar Terminal"),
|
("Enable terminal", "Habilitar terminal"),
|
||||||
("New tab", "Nova aba"),
|
("New tab", "Nova aba"),
|
||||||
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
|
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
|
||||||
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
|
("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)."),
|
("password-hidden-tip", "A senha permanente está definida como (oculta)."),
|
||||||
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
|
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
|
||||||
("Enable privacy mode", "Habilitar modo de privacidade"),
|
("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"),
|
("API Token", "Token de API"),
|
||||||
("Deploy", "Implantar"),
|
("Deploy", "Implantar"),
|
||||||
("Custom ID (optional)", "ID personalizado (opcional)"),
|
("Custom ID (optional)", "ID personalizado (opcional)"),
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
|
|
||||||
pub fn restart_remote_device(&self) {
|
pub fn restart_remote_device(&self) {
|
||||||
let mut lc = self.lc.write().unwrap();
|
let mut lc = self.lc.write().unwrap();
|
||||||
lc.restarting_remote_device = true;
|
lc.mark_restarting_remote_device();
|
||||||
let msg = lc.restart_remote_device();
|
let msg = lc.restart_remote_device();
|
||||||
self.send(Data::Message(msg));
|
self.send(Data::Message(msg));
|
||||||
}
|
}
|
||||||
@@ -656,7 +656,7 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_restarting_remote_device(&self) -> bool {
|
pub fn is_restarting_remote_device(&self) -> bool {
|
||||||
self.lc.read().unwrap().restarting_remote_device
|
self.lc.read().unwrap().is_restarting_remote_device()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
Reference in New Issue
Block a user