diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e5279a7e2..0111e5f90 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,7 +1,9 @@ import 'dart:io'; import 'dart:math'; +import 'package:desktop_drop/desktop_drop.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/models/file_model.dart'; import 'package:get/get.dart'; @@ -12,6 +14,8 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +enum LocationStatus { bread, textField } + class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -25,6 +29,23 @@ class _FileManagerPageState extends State final _localSelectedItems = 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; FileModel get model => _ffi.fileModel; @@ -44,6 +65,9 @@ class _FileManagerPageState extends State Wakelock.enable(); } print("init success with id ${widget.id}"); + // register location listener + _locationNodeLocal.addListener(onLocalLocationFocusChanged); + _locationNodeRemote.addListener(onRemoteLocationFocusChanged); } @override @@ -55,6 +79,8 @@ class _FileManagerPageState extends State Wakelock.disable(); } Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); super.dispose(); } @@ -112,7 +138,7 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { - final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; + final fd = model.getCurrentDir(isLocal); final entries = fd.entries; final sortIndex = (SortBy style) { switch (style) { @@ -129,222 +155,251 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; return Container( - decoration: BoxDecoration( - color: Colors.white54, border: Border.all(color: Colors.black26)), + decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - headTools(isLocal), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: DataTable( - showCheckboxColumn: true, - dataRowHeight: 25, - headingRowHeight: 30, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn(label: Text(translate(" "))), // icon - DataColumn( - label: Text( - translate("Name"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: entries.map((entry) { - final sizeStr = entry.isFile - ? readableFileSize(entry.size.toDouble()) - : ""; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - if (s != null) { - if (s) { - getSelectedItem(isLocal).add(isLocal, entry); - } else { - getSelectedItem(isLocal).remove(entry); - } - setState(() {}); - } - }, - selected: getSelectedItem(isLocal).contains(entry), - cells: [ - DataCell(Icon( - entry.isFile ? Icons.feed_outlined : Icons.folder, - size: 25)), - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 100), - child: Tooltip( - message: entry.name, - child: Text(entry.name, - overflow: TextOverflow.ellipsis), - )), onTap: () { - if (entry.isDirectory) { - model.openDirectory(entry.path, isLocal: isLocal); - if (isLocal) { - _localSelectedItems.clear(); + child: DropTarget( + onDragDone: (detail) => handleDragDone(detail, isLocal), + onDragEntered: (enter) { + _dropMaskVisible.value = true; + }, + onDragExited: (exit) { + _dropMaskVisible.value = false; + }, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + headTools(isLocal), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isEmpty + ? entries.where((element) { + if (searchText.isEmpty) { + return true; } else { - _remoteSelectedItems.clear(); + return element.name.contains(searchText.value); } - } else { - // Perform file-related tasks. - final _selectedItems = getSelectedItem(isLocal); - if (_selectedItems.contains(entry)) { - _selectedItems.remove(entry); - } else { - _selectedItems.add(isLocal, entry); - } - setState(() {}); - } - }), - DataCell(Text( - entry - .lastModified() - .toString() - .replaceAll(".000", "") + - " ", - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - DataCell(Text( - sizeStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - ]); - }).toList(), + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: true, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon + DataColumn( + label: Text( + translate("Name"), + ), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Name, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text( + translate("Modified"), + ), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Modified, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text(translate("Size")), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Size, + isLocal: isLocal, ascending: ascending); + }), + ], + rows: filteredEntries.map((entry) { + final sizeStr = entry.isFile + ? readableFileSize(entry.size.toDouble()) + : ""; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + if (s != null) { + if (s) { + getSelectedItem(isLocal) + .add(isLocal, entry); + } else { + getSelectedItem(isLocal).remove(entry); + } + setState(() {}); + } + }, + selected: + getSelectedItem(isLocal).contains(entry), + cells: [ + DataCell(Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 25)), + DataCell( + ConstrainedBox( + constraints: + BoxConstraints(maxWidth: 100), + child: Tooltip( + message: entry.name, + child: Text(entry.name, + overflow: TextOverflow.ellipsis), + )), onTap: () { + if (entry.isDirectory) { + openDirectory(entry.path, isLocal: isLocal); + if (isLocal) { + _localSelectedItems.clear(); + } else { + _remoteSelectedItems.clear(); + } + } else { + // Perform file-related tasks. + final _selectedItems = + getSelectedItem(isLocal); + if (_selectedItems.contains(entry)) { + _selectedItems.remove(entry); + } else { + _selectedItems.add(isLocal, entry); + } + setState(() {}); + } + }), + DataCell(Text( + entry + .lastModified() + .toString() + .replaceAll(".000", "") + + " ", + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), ), - ), - ) - ], - )), - // Center(child: listTail(isLocal: isLocal)), - // Expanded( - // child: ListView.builder( - // itemCount: entries.length + 1, - // itemBuilder: (context, index) { - // if (index >= entries.length) { - // return listTail(isLocal: isLocal); - // } - // var selected = false; - // if (model.selectMode) { - // selected = _selectedItems.contains(entries[index]); - // } - // - // final sizeStr = entries[index].isFile - // ? readableFileSize(entries[index].size.toDouble()) - // : ""; - // return Card( - // child: ListTile( - // leading: Icon( - // entries[index].isFile ? Icons.feed_outlined : Icons.folder, - // size: 40), - // title: Text(entries[index].name), - // selected: selected, - // subtitle: Text( - // entries[index] - // .lastModified() - // .toString() - // .replaceAll(".000", "") + - // " " + - // sizeStr, - // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - // ), - // trailing: needShowCheckBox() - // ? Checkbox( - // value: selected, - // onChanged: (v) { - // if (v == null) return; - // if (v && !selected) { - // _selectedItems.add(isLocal, entries[index]); - // } else if (!v && selected) { - // _selectedItems.remove(entries[index]); - // } - // setState(() {}); - // }) - // : PopupMenuButton( - // icon: Icon(Icons.more_vert), - // itemBuilder: (context) { - // return [ - // PopupMenuItem( - // child: Text(translate("Delete")), - // value: "delete", - // ), - // PopupMenuItem( - // child: Text(translate("Multi Select")), - // value: "multi_select", - // ), - // PopupMenuItem( - // child: Text(translate("Properties")), - // value: "properties", - // enabled: false, - // ) - // ]; - // }, - // onSelected: (v) { - // if (v == "delete") { - // final items = SelectedItems(); - // items.add(isLocal, entries[index]); - // model.removeAction(items); - // } else if (v == "multi_select") { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // } - // }), - // onTap: () { - // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { - // if (selected) { - // _selectedItems.remove(entries[index]); - // } else { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // return; - // } - // if (entries[index].isDirectory) { - // model.openDirectory(entries[index].path, isLocal: isLocal); - // breadCrumbScrollToEnd(isLocal); - // } else { - // // Perform file-related tasks. - // } - // }, - // onLongPress: () { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // if (model.selectMode) { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // }, - // ), - // ); - // }, - // )) - ]), + ) + ], + )), + // Center(child: listTail(isLocal: isLocal)), + // Expanded( + // child: ListView.builder( + // itemCount: entries.length + 1, + // itemBuilder: (context, index) { + // if (index >= entries.length) { + // return listTail(isLocal: isLocal); + // } + // var selected = false; + // if (model.selectMode) { + // selected = _selectedItems.contains(entries[index]); + // } + // + // final sizeStr = entries[index].isFile + // ? readableFileSize(entries[index].size.toDouble()) + // : ""; + // return Card( + // child: ListTile( + // leading: Icon( + // entries[index].isFile ? Icons.feed_outlined : Icons.folder, + // size: 40), + // title: Text(entries[index].name), + // selected: selected, + // subtitle: Text( + // entries[index] + // .lastModified() + // .toString() + // .replaceAll(".000", "") + + // " " + + // sizeStr, + // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + // ), + // trailing: needShowCheckBox() + // ? Checkbox( + // value: selected, + // onChanged: (v) { + // if (v == null) return; + // if (v && !selected) { + // _selectedItems.add(isLocal, entries[index]); + // } else if (!v && selected) { + // _selectedItems.remove(entries[index]); + // } + // setState(() {}); + // }) + // : PopupMenuButton( + // icon: Icon(Icons.more_vert), + // itemBuilder: (context) { + // return [ + // PopupMenuItem( + // child: Text(translate("Delete")), + // value: "delete", + // ), + // PopupMenuItem( + // child: Text(translate("Multi Select")), + // value: "multi_select", + // ), + // PopupMenuItem( + // child: Text(translate("Properties")), + // value: "properties", + // enabled: false, + // ) + // ]; + // }, + // onSelected: (v) { + // if (v == "delete") { + // final items = SelectedItems(); + // items.add(isLocal, entries[index]); + // model.removeAction(items); + // } else if (v == "multi_select") { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // } + // }), + // onTap: () { + // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { + // if (selected) { + // _selectedItems.remove(entries[index]); + // } else { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // return; + // } + // if (entries[index].isDirectory) { + // openDirectory(entries[index].path, isLocal: isLocal); + // breadCrumbScrollToEnd(isLocal); + // } else { + // // Perform file-related tasks. + // } + // }, + // onLongPress: () { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // if (model.selectMode) { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // }, + // ), + // ); + // }, + // )) + ]), + ), ); } @@ -355,8 +410,7 @@ class _FileManagerPageState extends State child: Container( margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Colors.white70, border: Border.all(color: Colors.grey)), + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( itemBuilder: (BuildContext context, int index) { @@ -383,7 +437,6 @@ class _FileManagerPageState extends State child: Text( '${item.jobName}', maxLines: 1, - style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis, )), Wrap( @@ -449,183 +502,213 @@ class _FileManagerPageState extends State model.goToParentDirectory(isLocal: isLocal); } - Widget headTools(bool isLocal) => Container( - child: Column( - children: [ - // symbols - PreferredSize( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); + Widget headTools(bool isLocal) { + final _locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + return Container( + child: Column( + children: [ + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration(color: Colors.blue), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Colors.white, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], + ), + preferredSize: Size(double.infinity, 70)), + // buttons + Row( + children: [ + Row( + children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: Icon(Icons.home_outlined)), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () { + goBack(isLocal: isLocal); + }, + ), + menu(isLocal: isLocal), + ], + ), + 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( + decoration: + BoxDecoration(border: Border.all(color: Colors.black12)), + child: Row( + children: [ + Expanded( + child: Obx(() => + _locationStatus.value == LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal))), + DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + openDirectory(path, isLocal: isLocal); } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( + }) + ], + )), + )), + PopupMenuButton( + itemBuilder: (context) => [ + 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( + onPressed: () { + model.refresh(isLocal: isLocal); + }, + icon: Icon(Icons.refresh)), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ IconButton( onPressed: () { - model.goHome(isLocal: isLocal); + final name = TextEditingController(); + _ffi.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model + .getCurrentDir( + isLocal) + .path, + name.value.text, + model.getCurrentIsWindows( + isLocal)), + isLocal: isLocal); + close(); + } + }, + child: Text(translate("OK"))) + ])); }, - icon: Icon(Icons.home_outlined)), + icon: Icon(Icons.create_new_folder_outlined)), IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: () { - goBack(isLocal: isLocal); - }, - ), - menu(isLocal: isLocal), + onPressed: () async { + final items = isLocal + ? _localSelectedItems + : _remoteSelectedItems; + await (model.removeAction(items, isLocal: isLocal)); + items.clear(); + }, + icon: Icon(Icons.delete_forever_outlined)), ], ), - Expanded( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black12)), - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: - Padding(padding: EdgeInsets.only(left: 4.0)), - suffix: DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem( - child: Text('/'), - value: '/', - ) - ], - onChanged: (path) { - if (path is String && path.isNotEmpty) { - model.openDirectory(path, isLocal: isLocal); - } - })), - controller: TextEditingController( - text: isLocal - ? model.currentLocalDir.path - : model.currentRemoteDir.path), - onSubmitted: (path) { - model.openDirectory(path, isLocal: isLocal); - }, - ))), - IconButton( - onPressed: () { - model.refresh(isLocal: isLocal); - }, - icon: Icon(Icons.refresh)) - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - final name = TextEditingController(); - _ffi.dialogManager.show((setState, close) => - CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir( - PathUtil.join( - model - .getCurrentDir(isLocal) - .path, - name.value.text, - model.getCurrentIsWindows( - isLocal)), - isLocal: isLocal); - close(); - } - }, - child: Text(translate("OK"))) - ])); - }, - icon: Icon(Icons.create_new_folder_outlined)), - IconButton( - onPressed: () async { - final items = isLocal - ? _localSelectedItems - : _remoteSelectedItems; - await (model.removeAction(items, isLocal: isLocal)); - items.clear(); - }, - icon: Icon(Icons.delete_forever_outlined)), - ], - ), - ), - TextButton.icon( - onPressed: () { - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: !isLocal); - items.clear(); - }, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: Icon( - Icons.send, - color: Colors.black54, - ), + ), + TextButton.icon( + onPressed: () { + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + items.clear(); + }, + icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send, ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - style: TextStyle( - color: Colors.black54, - ), - )), - ], - ).marginOnly(top: 8.0) - ], - )); + ), + label: Text( + isLocal ? translate('Send') : translate('Receive'), + )), + ], + ).marginOnly(top: 8.0) + ], + )); + } Widget listTail({bool isLocal = false}) { final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; @@ -663,4 +746,116 @@ class _FileManagerPageState extends State else if (platform != 'linux' && platform != 'android') platform = 'win'; 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 getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = model.getCurrentDir(isLocal).path; + final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + final breadCrumbList = List.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); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74be258a0..74c2cd515 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -68,6 +68,22 @@ class FileModel extends ChangeNotifier { 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 getCurrentHome(bool isLocal) { @@ -716,6 +732,7 @@ class FileModel extends ChangeNotifier { job.totalSize = total_size.toInt(); } debugPrint("update folder files: ${info}"); + notifyListeners(); } bool get remoteSortAscending => _remoteSortAscending; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fe7359bf5..6ccfe72ac 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -239,6 +239,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: "direct main" description: @@ -817,7 +824,7 @@ packages: name: qr_code_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" quiver: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a911903f8..40aa1ca43 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 dev_dependencies: flutter_launcher_icons: ^0.9.1