Merge pull request #1286 from Kingtous/flutter_desktop

feat: file transfer selectable navigation tools & search bar
This commit is contained in:
RustDesk 2022-08-16 14:04:33 +08:00 committed by GitHub
commit f797125ae2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 604 additions and 384 deletions

View File

@ -1,7 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -12,6 +14,8 @@ import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
enum LocationStatus { bread, textField }
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key); FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id; final String id;
@ -25,6 +29,23 @@ class _FileManagerPageState extends State<FileManagerPage>
final _localSelectedItems = SelectedItems(); final _localSelectedItems = SelectedItems();
final _remoteSelectedItems = SelectedItems(); final _remoteSelectedItems = SelectedItems();
final _locationStatusLocal = LocationStatus.bread.obs;
final _locationStatusRemote = LocationStatus.bread.obs;
final FocusNode _locationNodeLocal =
FocusNode(debugLabel: "locationNodeLocal");
final FocusNode _locationNodeRemote =
FocusNode(debugLabel: "locationNodeRemote");
final _searchTextLocal = "".obs;
final _searchTextRemote = "".obs;
final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController();
final _dropMaskVisible = false.obs;
ScrollController getBreadCrumbScrollController(bool isLocal) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
}
late FFI _ffi; late FFI _ffi;
FileModel get model => _ffi.fileModel; FileModel get model => _ffi.fileModel;
@ -44,6 +65,9 @@ class _FileManagerPageState extends State<FileManagerPage>
Wakelock.enable(); Wakelock.enable();
} }
print("init success with id ${widget.id}"); print("init success with id ${widget.id}");
// register location listener
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
} }
@override @override
@ -55,6 +79,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Wakelock.disable(); Wakelock.disable();
} }
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
_locationNodeLocal.removeListener(onLocalLocationFocusChanged);
_locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
super.dispose(); super.dispose();
} }
@ -112,7 +138,7 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
Widget body({bool isLocal = false}) { Widget body({bool isLocal = false}) {
final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; final fd = model.getCurrentDir(isLocal);
final entries = fd.entries; final entries = fd.entries;
final sortIndex = (SortBy style) { final sortIndex = (SortBy style) {
switch (style) { switch (style) {
@ -129,10 +155,17 @@ class _FileManagerPageState extends State<FileManagerPage>
final sortAscending = final sortAscending =
isLocal ? model.localSortAscending : model.remoteSortAscending; isLocal ? model.localSortAscending : model.remoteSortAscending;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
color: Colors.white54, border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0), margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: DropTarget(
onDragDone: (detail) => handleDragDone(detail, isLocal),
onDragEntered: (enter) {
_dropMaskVisible.value = true;
},
onDragExited: (exit) {
_dropMaskVisible.value = false;
},
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
headTools(isLocal), headTools(isLocal),
Expanded( Expanded(
@ -141,7 +174,19 @@ class _FileManagerPageState extends State<FileManagerPage>
children: [ children: [
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: DataTable( child: ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isEmpty
? entries.where((element) {
if (searchText.isEmpty) {
return true;
} else {
return element.name.contains(searchText.value);
}
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: true, showCheckboxColumn: true,
dataRowHeight: 25, dataRowHeight: 25,
headingRowHeight: 30, headingRowHeight: 30,
@ -174,7 +219,7 @@ class _FileManagerPageState extends State<FileManagerPage>
isLocal: isLocal, ascending: ascending); isLocal: isLocal, ascending: ascending);
}), }),
], ],
rows: entries.map((entry) { rows: filteredEntries.map((entry) {
final sizeStr = entry.isFile final sizeStr = entry.isFile
? readableFileSize(entry.size.toDouble()) ? readableFileSize(entry.size.toDouble())
: ""; : "";
@ -183,28 +228,33 @@ class _FileManagerPageState extends State<FileManagerPage>
onSelectChanged: (s) { onSelectChanged: (s) {
if (s != null) { if (s != null) {
if (s) { if (s) {
getSelectedItem(isLocal).add(isLocal, entry); getSelectedItem(isLocal)
.add(isLocal, entry);
} else { } else {
getSelectedItem(isLocal).remove(entry); getSelectedItem(isLocal).remove(entry);
} }
setState(() {}); setState(() {});
} }
}, },
selected: getSelectedItem(isLocal).contains(entry), selected:
getSelectedItem(isLocal).contains(entry),
cells: [ cells: [
DataCell(Icon( DataCell(Icon(
entry.isFile ? Icons.feed_outlined : Icons.folder, entry.isFile
? Icons.feed_outlined
: Icons.folder,
size: 25)), size: 25)),
DataCell( DataCell(
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(maxWidth: 100), constraints:
BoxConstraints(maxWidth: 100),
child: Tooltip( child: Tooltip(
message: entry.name, message: entry.name,
child: Text(entry.name, child: Text(entry.name,
overflow: TextOverflow.ellipsis), overflow: TextOverflow.ellipsis),
)), onTap: () { )), onTap: () {
if (entry.isDirectory) { if (entry.isDirectory) {
model.openDirectory(entry.path, isLocal: isLocal); openDirectory(entry.path, isLocal: isLocal);
if (isLocal) { if (isLocal) {
_localSelectedItems.clear(); _localSelectedItems.clear();
} else { } else {
@ -212,7 +262,8 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
} else { } else {
// Perform file-related tasks. // Perform file-related tasks.
final _selectedItems = getSelectedItem(isLocal); final _selectedItems =
getSelectedItem(isLocal);
if (_selectedItems.contains(entry)) { if (_selectedItems.contains(entry)) {
_selectedItems.remove(entry); _selectedItems.remove(entry);
} else { } else {
@ -236,7 +287,10 @@ class _FileManagerPageState extends State<FileManagerPage>
fontSize: 12, color: MyTheme.darkGray), fontSize: 12, color: MyTheme.darkGray),
)), )),
]); ]);
}).toList(), }).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
), ),
), ),
) )
@ -326,7 +380,7 @@ class _FileManagerPageState extends State<FileManagerPage>
// return; // return;
// } // }
// if (entries[index].isDirectory) { // if (entries[index].isDirectory) {
// model.openDirectory(entries[index].path, isLocal: isLocal); // openDirectory(entries[index].path, isLocal: isLocal);
// breadCrumbScrollToEnd(isLocal); // breadCrumbScrollToEnd(isLocal);
// } else { // } else {
// // Perform file-related tasks. // // Perform file-related tasks.
@ -345,6 +399,7 @@ class _FileManagerPageState extends State<FileManagerPage>
// }, // },
// )) // ))
]), ]),
),
); );
} }
@ -355,8 +410,7 @@ class _FileManagerPageState extends State<FileManagerPage>
child: Container( child: Container(
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration( decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
color: Colors.white70, border: Border.all(color: Colors.grey)),
child: Obx( child: Obx(
() => ListView.builder( () => ListView.builder(
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@ -383,7 +437,6 @@ class _FileManagerPageState extends State<FileManagerPage>
child: Text( child: Text(
'${item.jobName}', '${item.jobName}',
maxLines: 1, maxLines: 1,
style: TextStyle(color: Colors.black45),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
)), )),
Wrap( Wrap(
@ -449,7 +502,12 @@ class _FileManagerPageState extends State<FileManagerPage>
model.goToParentDirectory(isLocal: isLocal); model.goToParentDirectory(isLocal: isLocal);
} }
Widget headTools(bool isLocal) => Container( Widget headTools(bool isLocal) {
final _locationStatus =
isLocal ? _locationStatusLocal : _locationStatusRemote;
final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
return Container(
child: Column( child: Column(
children: [ children: [
// symbols // symbols
@ -501,16 +559,29 @@ class _FileManagerPageState extends State<FileManagerPage>
], ],
), ),
Expanded( Expanded(
child: GestureDetector(
onTap: () {
_locationStatus.value =
_locationStatus.value == LocationStatus.bread
? LocationStatus.textField
: LocationStatus.bread;
Future.delayed(Duration.zero, () {
if (_locationStatus.value == LocationStatus.textField) {
_locationFocus.requestFocus();
}
});
},
child: Container( child: Container(
decoration: BoxDecoration( decoration:
border: Border.all(color: Colors.black12)), BoxDecoration(border: Border.all(color: Colors.black12)),
child: TextField( child: Row(
decoration: InputDecoration( children: [
border: InputBorder.none, Expanded(
isDense: true, child: Obx(() =>
prefix: _locationStatus.value == LocationStatus.bread
Padding(padding: EdgeInsets.only(left: 4.0)), ? buildBread(isLocal)
suffix: DropdownButton<String>( : buildPathLocation(isLocal))),
DropdownButton<String>(
isDense: true, isDense: true,
underline: Offstage(), underline: Offstage(),
items: [ items: [
@ -522,22 +593,36 @@ class _FileManagerPageState extends State<FileManagerPage>
], ],
onChanged: (path) { onChanged: (path) {
if (path is String && path.isNotEmpty) { if (path is String && path.isNotEmpty) {
model.openDirectory(path, isLocal: isLocal); openDirectory(path, isLocal: isLocal);
} }
})), })
controller: TextEditingController( ],
text: isLocal )),
? model.currentLocalDir.path )),
: model.currentRemoteDir.path), PopupMenuButton(
onSubmitted: (path) { itemBuilder: (context) => [
model.openDirectory(path, isLocal: isLocal); PopupMenuItem(
}, enabled: false,
))), child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 200),
child: TextField(
controller:
TextEditingController(text: _searchTextObs.value),
autofocus: true,
decoration:
InputDecoration(prefixIcon: Icon(Icons.search)),
onChanged: (searchText) =>
onSearchText(searchText, isLocal),
),
))
],
child: Icon(Icons.search),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
model.refresh(isLocal: isLocal); model.refresh(isLocal: isLocal);
}, },
icon: Icon(Icons.refresh)) icon: Icon(Icons.refresh)),
], ],
), ),
Row( Row(
@ -551,8 +636,8 @@ class _FileManagerPageState extends State<FileManagerPage>
IconButton( IconButton(
onPressed: () { onPressed: () {
final name = TextEditingController(); final name = TextEditingController();
_ffi.dialogManager.show((setState, close) => _ffi.dialogManager
CustomAlertDialog( .show((setState, close) => CustomAlertDialog(
title: Text(translate("Create Folder")), title: Text(translate("Create Folder")),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -578,7 +663,8 @@ class _FileManagerPageState extends State<FileManagerPage>
model.createDir( model.createDir(
PathUtil.join( PathUtil.join(
model model
.getCurrentDir(isLocal) .getCurrentDir(
isLocal)
.path, .path,
name.value.text, name.value.text,
model.getCurrentIsWindows( model.getCurrentIsWindows(
@ -613,19 +699,16 @@ class _FileManagerPageState extends State<FileManagerPage>
angle: isLocal ? 0 : pi, angle: isLocal ? 0 : pi,
child: Icon( child: Icon(
Icons.send, Icons.send,
color: Colors.black54,
), ),
), ),
label: Text( label: Text(
isLocal ? translate('Send') : translate('Receive'), isLocal ? translate('Send') : translate('Receive'),
style: TextStyle(
color: Colors.black54,
),
)), )),
], ],
).marginOnly(top: 8.0) ).marginOnly(top: 8.0)
], ],
)); ));
}
Widget listTail({bool isLocal = false}) { Widget listTail({bool isLocal = false}) {
final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir;
@ -663,4 +746,116 @@ class _FileManagerPageState extends State<FileManagerPage>
else if (platform != 'linux' && platform != 'android') platform = 'win'; else if (platform != 'linux' && platform != 'android') platform = 'win';
return Image.asset('assets/$platform.png', width: 25, height: 25); return Image.asset('assets/$platform.png', width: 25, height: 25);
} }
void onLocalLocationFocusChanged() {
debugPrint("focus changed on local");
if (_locationNodeLocal.hasFocus) {
// ignore
} else {
// lost focus, change to bread
_locationStatusLocal.value = LocationStatus.bread;
}
}
void onRemoteLocationFocusChanged() {
debugPrint("focus changed on remote");
if (_locationNodeRemote.hasFocus) {
// ignore
} else {
// lost focus, change to bread
_locationStatusRemote.value = LocationStatus.bread;
}
}
Widget buildBread(bool isLocal) {
final items = getPathBreadCrumbItems(isLocal, (list) {
var path = "";
for (var item in list) {
path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal));
}
openDirectory(path, isLocal: isLocal);
});
return items.isEmpty
? Offstage()
: BreadCrumb(
items: items,
divider: Text("/").paddingSymmetric(horizontal: 4.0),
overflow: ScrollableOverflow(
controller: getBreadCrumbScrollController(isLocal)),
);
}
List<BreadCrumbItem> getPathBreadCrumbItems(
bool isLocal, void Function(List<String>) onPressed) {
final path = model.getCurrentDir(isLocal).path;
final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal));
final breadCrumbList = List<BreadCrumbItem>.empty(growable: true);
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
content: TextButton(
child: Text(e.value),
style:
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
return breadCrumbList;
}
breadCrumbScrollToEnd(bool isLocal) {
Future.delayed(Duration(milliseconds: 200), () {
final _breadCrumbScroller = getBreadCrumbScrollController(isLocal);
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
});
}
Widget buildPathLocation(bool isLocal) {
return TextField(
focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote,
decoration: InputDecoration(
border: InputBorder.none,
isDense: true,
prefix: Padding(padding: EdgeInsets.only(left: 4.0)),
),
controller:
TextEditingController(text: model.getCurrentDir(isLocal).path),
onSubmitted: (path) {
openDirectory(path, isLocal: isLocal);
},
);
}
onSearchText(String searchText, bool isLocal) {
if (isLocal) {
_searchTextLocal.value = searchText;
} else {
_searchTextRemote.value = searchText;
}
}
openDirectory(String path, {bool isLocal = false}) {
model.openDirectory(path, isLocal: isLocal).then((_) {
print("scroll");
breadCrumbScrollToEnd(isLocal);
});
}
void handleDragDone(DropDoneDetails details, bool isLocal) {
if (isLocal) {
// ignore local
return;
}
var items = SelectedItems();
details.files.forEach((file) {
final f = File(file.path);
items.add(
true,
Entry()
..path = file.path
..name = file.name
..size =
FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
});
model.sendFiles(items, isRemote: false);
}
} }

View File

@ -68,6 +68,22 @@ class FileModel extends ChangeNotifier {
return isLocal ? currentLocalDir : currentRemoteDir; return isLocal ? currentLocalDir : currentRemoteDir;
} }
String getCurrentShortPath(bool isLocal) {
final currentDir = getCurrentDir(isLocal);
final currentHome = getCurrentHome(isLocal);
if (currentDir.path.startsWith(currentHome)) {
var path = currentDir.path.replaceFirst(currentHome, "");
if (path.length == 0) return "";
if (path[0] == "/" || path[0] == "\\") {
// remove more '/' or '\'
path = path.replaceFirst(path[0], "");
}
return path;
} else {
return currentDir.path.replaceFirst(currentHome, "");
}
}
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
String getCurrentHome(bool isLocal) { String getCurrentHome(bool isLocal) {
@ -716,6 +732,7 @@ class FileModel extends ChangeNotifier {
job.totalSize = total_size.toInt(); job.totalSize = total_size.toInt();
} }
debugPrint("update folder files: ${info}"); debugPrint("update folder files: ${info}");
notifyListeners();
} }
bool get remoteSortAscending => _remoteSortAscending; bool get remoteSortAscending => _remoteSortAscending;

View File

@ -239,6 +239,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.12" version: "0.0.12"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
desktop_multi_window: desktop_multi_window:
dependency: "direct main" dependency: "direct main"
description: description:
@ -817,7 +824,7 @@ packages:
name: qr_code_scanner name: qr_code_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:

View File

@ -69,6 +69,7 @@ dependencies:
get: ^4.6.5 get: ^4.6.5
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
contextmenu: ^3.0.0 contextmenu: ^3.0.0
desktop_drop: ^0.3.3
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.9.1 flutter_launcher_icons: ^0.9.1