mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master'
This commit is contained in:
@@ -1453,10 +1453,12 @@ connectMainDesktop(String id,
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false}) async {
|
||||
bool isRDP = false}) async {
|
||||
if (id == '') return;
|
||||
id = id.replaceAll(' ', '');
|
||||
final oldId = id;
|
||||
id = await bind.mainHandleRelayId(id: id);
|
||||
final forceRelay = id != oldId;
|
||||
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||
"more than one connect type");
|
||||
|
||||
@@ -1819,3 +1821,19 @@ class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
|
||||
@override
|
||||
bool get allowImplicitScrolling => false;
|
||||
}
|
||||
|
||||
Widget futureBuilder(
|
||||
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return hasData(snapshot.data!);
|
||||
} else {
|
||||
if (snapshot.hasError) {
|
||||
debugPrint(snapshot.error.toString());
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -515,15 +515,31 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
String id, Future<void> Function() reloadFunc,
|
||||
{bool isLan = false}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Remove'),
|
||||
style: style,
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Delete'),
|
||||
style: style?.copyWith(color: Colors.red),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Icon(Icons.delete_forever, color: Colors.red),
|
||||
),
|
||||
).marginOnly(right: 4)),
|
||||
],
|
||||
),
|
||||
proc: () {
|
||||
() async {
|
||||
if (isLan) {
|
||||
// TODO
|
||||
bind.mainRemoveDiscovered(id: id);
|
||||
} else {
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
if (favs.remove(id)) {
|
||||
await bind.mainStoreFav(favs: favs);
|
||||
}
|
||||
await bind.mainRemovePeer(id: id);
|
||||
}
|
||||
removePreference(id);
|
||||
@@ -553,9 +569,21 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
@protected
|
||||
MenuEntryBase<String> _addFavAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Add to Favorites'),
|
||||
style: style,
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Add to Favorites'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Icon(Icons.star_outline),
|
||||
),
|
||||
).marginOnly(right: 4)),
|
||||
],
|
||||
),
|
||||
proc: () {
|
||||
() async {
|
||||
@@ -575,9 +603,21 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
MenuEntryBase<String> _rmFavAction(
|
||||
String id, Future<void> Function() reloadFunc) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Remove from Favorites'),
|
||||
style: style,
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Remove from Favorites'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Icon(Icons.star),
|
||||
),
|
||||
).marginOnly(right: 4)),
|
||||
],
|
||||
),
|
||||
proc: () {
|
||||
() async {
|
||||
@@ -642,8 +682,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: translate('Name')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -677,6 +718,9 @@ class RecentPeerCard extends BasePeerCard {
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != 'Android') {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
}
|
||||
@@ -690,16 +734,26 @@ class RecentPeerCard extends BasePeerCard {
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadRecentPeers();
|
||||
}));
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
menuItems.add(_addFavAction(peer.id));
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
|
||||
if (!favs.contains(peer.id)) {
|
||||
menuItems.add(_addFavAction(peer.id));
|
||||
} else {
|
||||
menuItems.add(_rmFavAction(peer.id, () async {}));
|
||||
}
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadRecentPeers();
|
||||
}));
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
@@ -732,18 +786,23 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadFavPeers();
|
||||
}));
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
menuItems.add(_rmFavAction(peer.id, () async {
|
||||
await bind.mainLoadFavPeers();
|
||||
}));
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
await bind.mainLoadFavPeers();
|
||||
}));
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
@@ -763,6 +822,9 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != 'Android') {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
}
|
||||
@@ -774,11 +836,28 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
if (Platform.isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id, () async {}));
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
|
||||
final inRecent = await bind.mainIsInRecentPeers(id: peer.id);
|
||||
if (inRecent) {
|
||||
if (!favs.contains(peer.id)) {
|
||||
menuItems.add(_addFavAction(peer.id));
|
||||
} else {
|
||||
menuItems.add(_rmFavAction(peer.id, () async {}));
|
||||
}
|
||||
}
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(
|
||||
_removeAction(peer.id, () async {
|
||||
await bind.mainLoadLanPeers();
|
||||
}, isLan: true),
|
||||
);
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
@@ -811,13 +890,15 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
menuItems.add(_removeAction(peer.id, () async {}));
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
if (gFFI.abModel.tags.isNotEmpty) {
|
||||
menuItems.add(_editTagAction(peer.id));
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id, () async {}));
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ const int kDesktopMaxDisplayHeight = 1080;
|
||||
|
||||
const double kDesktopFileTransferNameColWidth = 200;
|
||||
const double kDesktopFileTransferModifiedColWidth = 120;
|
||||
const double kDesktopFileTransferMinimumWidth = 100;
|
||||
const double kDesktopFileTransferMaximumWidth = 300;
|
||||
const double kDesktopFileTransferRowHeight = 30.0;
|
||||
const double kDesktopFileTransferHeaderHeight = 25.0;
|
||||
|
||||
|
||||
@@ -151,10 +151,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
var id = _idController.id;
|
||||
var forceRelay = id.endsWith(r'/r');
|
||||
if (forceRelay) id = id.substring(0, id.length - 2);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer, forceRelay: forceRelay);
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
|
||||
@@ -319,7 +319,7 @@ class _GeneralState extends State<_General> {
|
||||
bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (Platform.isWindows) {
|
||||
devices.insert(0, 'System Sound');
|
||||
@@ -346,7 +346,7 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget record(BuildContext context) {
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
String customDirectory =
|
||||
await bind.mainGetOption(key: 'video-save-directory');
|
||||
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
|
||||
@@ -399,7 +399,7 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget language() {
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
String langs = await bind.mainGetLangs();
|
||||
String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
||||
return {'langs': langs, 'lang': lang};
|
||||
@@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
Widget _permissions(context, bool stopService) {
|
||||
bool enabled = !locked;
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.mainGetOption(key: 'access-mode');
|
||||
}(), hasData: (data) {
|
||||
String accessMode = data! as String;
|
||||
@@ -744,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
return [
|
||||
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
|
||||
update: update, enabled: !locked),
|
||||
_futureBuilder(
|
||||
futureBuilder(
|
||||
future: () async {
|
||||
String enabled = await bind.mainGetOption(key: 'direct-server');
|
||||
String port = await bind.mainGetOption(key: 'direct-access-port');
|
||||
@@ -805,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
Widget whitelist() {
|
||||
bool enabled = !locked;
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.mainGetOption(key: 'whitelist');
|
||||
}(), hasData: (data) {
|
||||
RxBool hasWhitelist = (data as String).isNotEmpty.obs;
|
||||
@@ -931,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
|
||||
server(bool enabled) {
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.mainGetOptions();
|
||||
}(), hasData: (data) {
|
||||
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
|
||||
@@ -1366,7 +1366,7 @@ class _About extends StatefulWidget {
|
||||
class _AboutState extends State<_About> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _futureBuilder(future: () async {
|
||||
return futureBuilder(future: () async {
|
||||
final license = await bind.mainGetLicense();
|
||||
final version = await bind.mainGetVersion();
|
||||
final buildDate = await bind.mainGetBuildDate();
|
||||
@@ -1500,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
||||
bool enabled = true,
|
||||
Icon? checkedIcon,
|
||||
bool? fakeValue}) {
|
||||
return _futureBuilder(
|
||||
return futureBuilder(
|
||||
future: bind.mainGetOption(key: key),
|
||||
hasData: (data) {
|
||||
bool value = option2bool(key, data.toString());
|
||||
@@ -1633,22 +1633,6 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
|
||||
).marginOnly(left: _kContentHSubMargin);
|
||||
}
|
||||
|
||||
Widget _futureBuilder(
|
||||
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return hasData(snapshot.data!);
|
||||
} else {
|
||||
if (snapshot.hasError) {
|
||||
debugPrint(snapshot.error.toString());
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _lock(
|
||||
bool locked,
|
||||
String label,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -78,6 +79,10 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
|
||||
final _listSearchBufferLocal = TimeoutStringBuffer();
|
||||
final _listSearchBufferRemote = TimeoutStringBuffer();
|
||||
final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs;
|
||||
final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs;
|
||||
final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs;
|
||||
final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs;
|
||||
|
||||
/// [_lastClickTime], [_lastClickEntry] help to handle double click
|
||||
int _lastClickTime =
|
||||
@@ -297,11 +302,12 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
var searchResult = entries
|
||||
.skip(skipCount)
|
||||
.where((element) => element.name.startsWith(buffer));
|
||||
.where((element) => element.name.toLowerCase().startsWith(buffer));
|
||||
if (searchResult.isEmpty) {
|
||||
// cannot find next, lets restart search from head
|
||||
debugPrint("restart search from head");
|
||||
searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
entries.where((element) => element.name.toLowerCase().startsWith(buffer));
|
||||
}
|
||||
if (searchResult.isEmpty) {
|
||||
setState(() {
|
||||
@@ -310,13 +316,13 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return;
|
||||
}
|
||||
_jumpToEntry(isLocal, searchResult.first, scrollController,
|
||||
kDesktopFileTransferRowHeight, buffer);
|
||||
kDesktopFileTransferRowHeight);
|
||||
},
|
||||
onSearch: (buffer) {
|
||||
debugPrint("searching for $buffer");
|
||||
final selectedEntries = getSelectedItems(isLocal);
|
||||
final searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
entries.where((element) => element.name.toLowerCase().startsWith(buffer));
|
||||
selectedEntries.clear();
|
||||
if (searchResult.isEmpty) {
|
||||
setState(() {
|
||||
@@ -325,7 +331,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return;
|
||||
}
|
||||
_jumpToEntry(isLocal, searchResult.first, scrollController,
|
||||
kDesktopFileTransferRowHeight, buffer);
|
||||
kDesktopFileTransferRowHeight);
|
||||
},
|
||||
child: ObxValue<RxString>(
|
||||
(searchText) {
|
||||
@@ -362,37 +368,41 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
width: kDesktopFileTransferNameColWidth,
|
||||
child: Tooltip(
|
||||
waitDuration:
|
||||
Duration(milliseconds: 500),
|
||||
message: entry.name,
|
||||
child: Row(children: [
|
||||
entry.isDrive
|
||||
? Image(
|
||||
image: iconHardDrive,
|
||||
fit: BoxFit.scaleDown,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withOpacity(0.7))
|
||||
.paddingAll(4)
|
||||
: SvgPicture.asset(
|
||||
entry.isFile
|
||||
? "assets/file.svg"
|
||||
: "assets/folder.svg",
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.name.nonBreaking,
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
child: Obx(
|
||||
() => Container(
|
||||
width: isLocal
|
||||
? _nameColWidthLocal.value
|
||||
: _nameColWidthRemote.value,
|
||||
child: Tooltip(
|
||||
waitDuration:
|
||||
Duration(milliseconds: 500),
|
||||
message: entry.name,
|
||||
child: Row(children: [
|
||||
entry.isDrive
|
||||
? Image(
|
||||
image: iconHardDrive,
|
||||
fit: BoxFit.scaleDown,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withOpacity(0.7))
|
||||
.paddingAll(4)
|
||||
: SvgPicture.asset(
|
||||
entry.isFile
|
||||
? "assets/file.svg"
|
||||
: "assets/folder.svg",
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.name.nonBreaking,
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
final items = getSelectedItems(isLocal);
|
||||
// handle double click
|
||||
@@ -406,24 +416,35 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 2.0,
|
||||
),
|
||||
GestureDetector(
|
||||
child: SizedBox(
|
||||
width: kDesktopFileTransferModifiedColWidth,
|
||||
child: Tooltip(
|
||||
waitDuration:
|
||||
Duration(milliseconds: 500),
|
||||
message: lastModifiedStr,
|
||||
child: Text(
|
||||
lastModifiedStr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
child: Obx(
|
||||
() => SizedBox(
|
||||
width: isLocal
|
||||
? _modifiedColWidthLocal.value
|
||||
: _modifiedColWidthRemote.value,
|
||||
child: Tooltip(
|
||||
waitDuration:
|
||||
Duration(milliseconds: 500),
|
||||
message: lastModifiedStr,
|
||||
child: Text(
|
||||
lastModifiedStr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Divider from header.
|
||||
SizedBox(
|
||||
width: 100,
|
||||
width: 2.0,
|
||||
),
|
||||
Expanded(
|
||||
// width: 100,
|
||||
child: GestureDetector(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
@@ -450,7 +471,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildFileBrowserHeader(context, isLocal),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildFileBrowserHeader(context, isLocal)),
|
||||
],
|
||||
),
|
||||
// Body
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
@@ -472,7 +497,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
|
||||
void _jumpToEntry(bool isLocal, Entry entry,
|
||||
ScrollController scrollController, double rowHeight, String buffer) {
|
||||
ScrollController scrollController, double rowHeight) {
|
||||
final entries = model.getCurrentDir(isLocal).entries;
|
||||
final index = entries.indexOf(entry);
|
||||
if (index == -1) {
|
||||
@@ -480,7 +505,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
final selectedEntries = getSelectedItems(isLocal);
|
||||
final searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
entries.where((element) => element == entry);
|
||||
selectedEntries.clear();
|
||||
if (searchResult.isEmpty) {
|
||||
return;
|
||||
@@ -1396,17 +1421,23 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
height: kDesktopFileTransferHeaderHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: headerTextStyle,
|
||||
).marginSymmetric(horizontal: 4),
|
||||
ascending.value != null
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
name,
|
||||
style: headerTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).marginSymmetric(horizontal: 4),
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: ascending.value != null
|
||||
? Icon(
|
||||
ascending.value!
|
||||
? Icons.keyboard_arrow_up_rounded
|
||||
: Icons.keyboard_arrow_down_rounded,
|
||||
)
|
||||
: const Offstage()
|
||||
: const Offstage())
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1420,16 +1451,48 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
|
||||
Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
|
||||
return Row(
|
||||
children: [
|
||||
headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name,
|
||||
translate("Name"), isLocal),
|
||||
headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified,
|
||||
translate("Modified"), isLocal),
|
||||
Expanded(
|
||||
child:
|
||||
headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
|
||||
],
|
||||
final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote;
|
||||
final modifiedColWidth =
|
||||
isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote;
|
||||
final padding = EdgeInsets.all(1.0);
|
||||
return SizedBox(
|
||||
height: kDesktopFileTransferHeaderHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => headerItemFunc(
|
||||
nameColWidth.value, SortBy.name, translate("Name"), isLocal),
|
||||
),
|
||||
DraggableDivider(
|
||||
axis: Axis.vertical,
|
||||
onPointerMove: (dx) {
|
||||
nameColWidth.value += dx;
|
||||
nameColWidth.value = min(
|
||||
kDesktopFileTransferMaximumWidth,
|
||||
max(kDesktopFileTransferMinimumWidth,
|
||||
nameColWidth.value));
|
||||
},
|
||||
padding: padding,
|
||||
),
|
||||
Obx(
|
||||
() => headerItemFunc(modifiedColWidth.value, SortBy.modified,
|
||||
translate("Modified"), isLocal),
|
||||
),
|
||||
DraggableDivider(
|
||||
axis: Axis.vertical,
|
||||
onPointerMove: (dx) {
|
||||
modifiedColWidth.value += dx;
|
||||
modifiedColWidth.value = min(
|
||||
kDesktopFileTransferMaximumWidth,
|
||||
max(kDesktopFileTransferMinimumWidth,
|
||||
modifiedColWidth.value));
|
||||
},
|
||||
padding: padding),
|
||||
Expanded(
|
||||
child:
|
||||
headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
53
flutter/lib/desktop/widgets/dragable_divider.dart
Normal file
53
flutter/lib/desktop/widgets/dragable_divider.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter/src/widgets/placeholder.dart';
|
||||
|
||||
class DraggableDivider extends StatefulWidget {
|
||||
final Axis axis;
|
||||
final double thickness;
|
||||
final Color color;
|
||||
final Function(double)? onPointerMove;
|
||||
final VoidCallback? onHover;
|
||||
final EdgeInsets padding;
|
||||
const DraggableDivider({
|
||||
super.key,
|
||||
this.axis = Axis.horizontal,
|
||||
this.thickness = 1.0,
|
||||
this.color = const Color.fromARGB(200, 177, 175, 175),
|
||||
this.onPointerMove,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 1.0),
|
||||
this.onHover,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DraggableDivider> createState() => _DraggableDividerState();
|
||||
}
|
||||
|
||||
class _DraggableDividerState extends State<DraggableDivider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerMove: (event) {
|
||||
final dl =
|
||||
widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx;
|
||||
widget.onPointerMove?.call(dl);
|
||||
},
|
||||
onPointerHover: (event) => widget.onHover?.call(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeLeftRight,
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: widget.color),
|
||||
width: widget.axis == Axis.horizontal
|
||||
? double.infinity
|
||||
: widget.thickness,
|
||||
height: widget.axis == Axis.horizontal
|
||||
? widget.thickness
|
||||
: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ class TimeoutStringBuffer {
|
||||
}
|
||||
|
||||
ListSearchAction input(String ch) {
|
||||
ch = ch.toLowerCase();
|
||||
final curr = DateTime.now();
|
||||
try {
|
||||
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -374,8 +374,7 @@ void showWaitUacDialog(
|
||||
));
|
||||
}
|
||||
|
||||
void _showRequestElevationDialog(
|
||||
String id, OverlayDialogManager dialogManager) {
|
||||
void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
|
||||
RxString groupValue = ''.obs;
|
||||
RxString errUser = ''.obs;
|
||||
RxString errPwd = ''.obs;
|
||||
@@ -531,7 +530,7 @@ void showOnBlockDialog(
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
_showRequestElevationDialog(id, dialogManager);
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
@@ -553,7 +552,7 @@ void showElevationError(String id, String type, String title, String text,
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
_showRequestElevationDialog(id, dialogManager);
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
|
||||
@@ -459,17 +459,22 @@ class InputModel {
|
||||
}
|
||||
evt['type'] = type;
|
||||
if (isDesktop) {
|
||||
y = y - stateGlobal.tabBarHeight;
|
||||
y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value;
|
||||
x -= stateGlobal.windowBorderWidth.value;
|
||||
}
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
||||
var nearBottom = (canvasModel.size.height - y) < nearThr;
|
||||
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
if (isMove) {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
final d = ffiModel.display;
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
x += imageWidth * canvasModel.scrollX;
|
||||
y += imageHeight * canvasModel.scrollY;
|
||||
|
||||
@@ -487,6 +492,15 @@ class InputModel {
|
||||
|
||||
x /= canvasModel.scale;
|
||||
y /= canvasModel.scale;
|
||||
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
|
||||
final step = 1.0 / canvasModel.scale - 1;
|
||||
if (nearRight) {
|
||||
x += step;
|
||||
}
|
||||
if (nearBottom) {
|
||||
y += step;
|
||||
}
|
||||
}
|
||||
x += d.x;
|
||||
y += d.y;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? '');
|
||||
@@ -203,6 +203,8 @@ class FfiModel with ChangeNotifier {
|
||||
final peer_id = evt['peer_id'].toString();
|
||||
await bind.sessionSwitchSides(id: peer_id);
|
||||
closeConnection(id: peer_id);
|
||||
} else if (name == 'portable_service_running') {
|
||||
parent.target?.elevationModel.onPortableServiceRunning(evt);
|
||||
} else if (name == "on_url_scheme_received") {
|
||||
final url = evt['url'].toString();
|
||||
parseRustdeskUri(url);
|
||||
@@ -239,37 +241,35 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
|
||||
final oldOrientation = _display.width > _display.height;
|
||||
var old = _pi.currentDisplay;
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
_display.x = double.parse(evt['x']);
|
||||
_display.y = double.parse(evt['y']);
|
||||
_display.width = int.parse(evt['width']);
|
||||
_display.height = int.parse(evt['height']);
|
||||
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
|
||||
if (old != _pi.currentDisplay) {
|
||||
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||
_updateCurDisplay(String peerId, Display newDisplay) {
|
||||
if (newDisplay != _display) {
|
||||
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
|
||||
parent.target?.cursorModel
|
||||
.updateDisplayOrigin(newDisplay.x, newDisplay.y);
|
||||
}
|
||||
_display = newDisplay;
|
||||
_updateSessionWidthHeight(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(peerId, display.width, display.height);
|
||||
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
var newDisplay = Display();
|
||||
newDisplay.x = double.parse(evt['x']);
|
||||
newDisplay.y = double.parse(evt['y']);
|
||||
newDisplay.width = int.parse(evt['width']);
|
||||
newDisplay.height = int.parse(evt['height']);
|
||||
newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
|
||||
|
||||
_updateCurDisplay(peerId, newDisplay);
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
// remote is mobile, and orientation changed
|
||||
if ((_display.width > _display.height) != oldOrientation) {
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
}
|
||||
if (_pi.platform == kPeerPlatformLinux ||
|
||||
_pi.platform == kPeerPlatformWindows ||
|
||||
_pi.platform == kPeerPlatformMacOS) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
}
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -369,7 +369,8 @@ class FfiModel with ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(String id, int width, int height) {
|
||||
_updateSessionWidthHeight(String id) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
bind.sessionSetSize(id: id, width: display.width, height: display.height);
|
||||
}
|
||||
|
||||
@@ -426,7 +427,7 @@ class FfiModel with ChangeNotifier {
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay < _pi.displays.length) {
|
||||
_display = _pi.displays[_pi.currentDisplay];
|
||||
_updateSessionWidthHeight(peerId, display.width, display.height);
|
||||
_updateSessionWidthHeight(peerId);
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
parent.target?.dialogManager.showLoading(
|
||||
@@ -437,10 +438,36 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
Map<String, dynamic> features = json.decode(evt['features']);
|
||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
parent.target?.elevationModel.onPeerInfo(_pi);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
handleResolutions(String id, dynamic resolutions) {
|
||||
try {
|
||||
final List<dynamic> dynamicArray = jsonDecode(resolutions as String);
|
||||
List<Resolution> arr = List.empty(growable: true);
|
||||
for (int i = 0; i < dynamicArray.length; i++) {
|
||||
var width = dynamicArray[i]["width"];
|
||||
var height = dynamicArray[i]["height"];
|
||||
if (width is int && width > 0 && height is int && height > 0) {
|
||||
arr.add(Resolution(width, height));
|
||||
}
|
||||
}
|
||||
arr.sort((a, b) {
|
||||
if (b.width != a.width) {
|
||||
return b.width - a.width;
|
||||
} else {
|
||||
return b.height - a.height;
|
||||
}
|
||||
});
|
||||
_pi.resolutions = arr;
|
||||
} catch (e) {
|
||||
debugPrint("Failed to parse resolutions:$e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the peer info synchronization event based on [evt].
|
||||
handleSyncPeerInfo(Map<String, dynamic> evt, String peerId) async {
|
||||
if (evt['displays'] != null) {
|
||||
@@ -458,6 +485,9 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
_pi.displays = newDisplays;
|
||||
stateGlobal.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
|
||||
_updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -765,12 +795,18 @@ class CanvasModel with ChangeNotifier {
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
var dxOffset = 0;
|
||||
var dyOffset = 0;
|
||||
if (dw > size.width) {
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
try {
|
||||
if (dw > size.width) {
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
}
|
||||
} catch (e) {
|
||||
// Unhandled Exception: Unsupported operation: Infinity or NaN toInt
|
||||
return;
|
||||
}
|
||||
|
||||
_x += dxOffset;
|
||||
_y += dyOffset;
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
@@ -1366,6 +1402,21 @@ class RecordingModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class ElevationModel with ChangeNotifier {
|
||||
WeakReference<FFI> parent;
|
||||
ElevationModel(this.parent);
|
||||
bool _running = false;
|
||||
bool _canElevate = false;
|
||||
bool get showRequestMenu => _canElevate && !_running;
|
||||
onPeerInfo(PeerInfo pi) {
|
||||
_canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false;
|
||||
}
|
||||
|
||||
onPortableServiceRunning(Map<String, dynamic> evt) {
|
||||
_running = evt['running'] == 'true';
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
|
||||
|
||||
/// Flutter state manager and data communication with the Rust core.
|
||||
@@ -1391,6 +1442,7 @@ class FFI {
|
||||
late final QualityMonitorModel qualityMonitorModel; // session
|
||||
late final RecordingModel recordingModel; // session
|
||||
late final InputModel inputModel; // session
|
||||
late final ElevationModel elevationModel; // session
|
||||
|
||||
FFI() {
|
||||
imageModel = ImageModel(WeakReference(this));
|
||||
@@ -1407,6 +1459,7 @@ class FFI {
|
||||
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||
recordingModel = RecordingModel(WeakReference(this));
|
||||
inputModel = InputModel(WeakReference(this));
|
||||
elevationModel = ElevationModel(WeakReference(this));
|
||||
}
|
||||
|
||||
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
|
||||
@@ -1530,6 +1583,30 @@ class Display {
|
||||
? kDesktopDefaultDisplayHeight
|
||||
: kMobileDefaultDisplayHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Display &&
|
||||
other.runtimeType == runtimeType &&
|
||||
_innerEqual(other);
|
||||
|
||||
bool _innerEqual(Display other) =>
|
||||
other.x == x &&
|
||||
other.y == y &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.cursorEmbedded == cursorEmbedded;
|
||||
}
|
||||
|
||||
class Resolution {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
Resolution(this.width, this.height);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Resolution($width,$height)';
|
||||
}
|
||||
}
|
||||
|
||||
class Features {
|
||||
@@ -1545,6 +1622,7 @@ class PeerInfo {
|
||||
int currentDisplay = 0;
|
||||
List<Display> displays = [];
|
||||
Features features = Features();
|
||||
List<Resolution> resolutions = [];
|
||||
}
|
||||
|
||||
const canvasKey = 'canvas';
|
||||
|
||||
Reference in New Issue
Block a user