From d9c93655204665b6da79b182de6170aa0a88e4da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 11:46:51 +0800 Subject: [PATCH 1/5] feat: switch breadcrumb&path with focus node Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 456 +++++++++++------- flutter/lib/models/file_model.dart | 16 + 2 files changed, 297 insertions(+), 175 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e5279a7e2..9febc462b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; 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 +13,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 +28,17 @@ 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 FocusNode _locationSearchLocal = + FocusNode(debugLabel: "locationSearchLocal"); + final FocusNode _locationSearchRemote = + FocusNode(debugLabel: "locationSearchRemote"); + late FFI _ffi; FileModel get model => _ffi.fileModel; @@ -44,6 +58,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 +72,8 @@ class _FileManagerPageState extends State Wakelock.disable(); } Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); super.dispose(); } @@ -129,8 +148,7 @@ 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: [ @@ -142,6 +160,7 @@ class _FileManagerPageState extends State Expanded( child: SingleChildScrollView( child: DataTable( + key: ValueKey(isLocal ? 0 : 1), showCheckboxColumn: true, dataRowHeight: 25, headingRowHeight: 30, @@ -223,9 +242,9 @@ class _FileManagerPageState extends State }), DataCell(Text( entry - .lastModified() - .toString() - .replaceAll(".000", "") + + .lastModified() + .toString() + .replaceAll(".000", "") + " ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), @@ -355,8 +374,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) { @@ -449,183 +467,206 @@ 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; + 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) { + model.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( + decoration: InputDecoration(), + ), + )) + ], + 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 +704,69 @@ 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 directory = model.getCurrentDir(isLocal); + print(directory.path); + return BreadCrumb( + items: getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + model.openDirectory(path, isLocal: isLocal); + }), + divider: Text("/").paddingSymmetric(horizontal: 4.0), + ); + } + + 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; + } + + 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) { + model.openDirectory(path, isLocal: isLocal); + }, + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74be258a0..1c3960b50 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) { From 2017a0f02b8ad41345ee6634f6ff611730370f22 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:06:54 +0800 Subject: [PATCH 2/5] feat: file transfer searchbar Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 242 ++++++++++-------- 1 file changed, 139 insertions(+), 103 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9febc462b..f2c752f1e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -34,10 +34,8 @@ class _FileManagerPageState extends State FocusNode(debugLabel: "locationNodeLocal"); final FocusNode _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); - final FocusNode _locationSearchLocal = - FocusNode(debugLabel: "locationSearchLocal"); - final FocusNode _locationSearchRemote = - FocusNode(debugLabel: "locationSearchRemote"); + final searchTextLocal = "".obs; + final searchTextRemote = "".obs; late FFI _ffi; @@ -131,7 +129,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) { @@ -159,103 +157,127 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - child: 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: 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(); - } else { - _remoteSelectedItems.clear(); + child: Obx( + () { + final filteredEntries = entries.where((element) { + if (isLocal) { + if (searchTextLocal.isEmpty) { + return true; + } else { + return element.name.contains(searchTextLocal.value); + } + } else { + if (searchTextRemote.isEmpty) { + return true; + } else { + return element.name.contains(searchTextRemote.value); + } + } + }).toList(growable: false); + 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(() {}); } - } 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(), + }, + 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(); + } 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(), + ); + }, ), ), ) @@ -401,7 +423,6 @@ class _FileManagerPageState extends State child: Text( '${item.jobName}', maxLines: 1, - style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis, )), Wrap( @@ -471,6 +492,7 @@ class _FileManagerPageState extends State final _locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final _searchTextObs = isLocal ? searchTextLocal : searchTextRemote; return Container( child: Column( children: [ @@ -570,7 +592,13 @@ class _FileManagerPageState extends State child: ConstrainedBox( constraints: BoxConstraints(minWidth: 200), child: TextField( - decoration: InputDecoration(), + controller: + TextEditingController(text: _searchTextObs.value), + autofocus: true, + decoration: + InputDecoration(prefixIcon: Icon(Icons.search)), + onChanged: (searchText) => + onSearchText(searchText, isLocal), ), )) ], @@ -769,4 +797,12 @@ class _FileManagerPageState extends State }, ); } + + onSearchText(String searchText, bool isLocal) { + if (isLocal) { + searchTextLocal.value = searchText; + } else { + searchTextRemote.value = searchText; + } + } } From eea62352d21e7240520b6b8e3ac474bffbb4b2d1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:28:12 +0800 Subject: [PATCH 3/5] feat: file transfer path scrollable Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index f2c752f1e..a35ce4378 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -34,8 +34,14 @@ class _FileManagerPageState extends State FocusNode(debugLabel: "locationNodeLocal"); final FocusNode _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); - final searchTextLocal = "".obs; - final searchTextRemote = "".obs; + final _searchTextLocal = "".obs; + final _searchTextRemote = "".obs; + final _breadCrumbScrollerLocal = ScrollController(); + final _breadCrumbScrollerRemote = ScrollController(); + + ScrollController getBreadCrumbScrollController(bool isLocal) { + return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; + } late FFI _ffi; @@ -159,19 +165,13 @@ class _FileManagerPageState extends State child: SingleChildScrollView( child: Obx( () { + final searchText = + isLocal ? _searchTextLocal : _searchTextRemote; final filteredEntries = entries.where((element) { - if (isLocal) { - if (searchTextLocal.isEmpty) { - return true; - } else { - return element.name.contains(searchTextLocal.value); - } + if (searchText.isEmpty) { + return true; } else { - if (searchTextRemote.isEmpty) { - return true; - } else { - return element.name.contains(searchTextRemote.value); - } + return element.name.contains(searchText.value); } }).toList(growable: false); return DataTable( @@ -234,15 +234,14 @@ class _FileManagerPageState extends State DataCell( ConstrainedBox( constraints: - BoxConstraints(maxWidth: 100), + 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); + openDirectory(entry.path, isLocal: isLocal); if (isLocal) { _localSelectedItems.clear(); } else { @@ -251,7 +250,7 @@ class _FileManagerPageState extends State } else { // Perform file-related tasks. final _selectedItems = - getSelectedItem(isLocal); + getSelectedItem(isLocal); if (_selectedItems.contains(entry)) { _selectedItems.remove(entry); } else { @@ -262,9 +261,9 @@ class _FileManagerPageState extends State }), DataCell(Text( entry - .lastModified() - .toString() - .replaceAll(".000", "") + + .lastModified() + .toString() + .replaceAll(".000", "") + " ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), @@ -367,7 +366,7 @@ class _FileManagerPageState extends State // return; // } // if (entries[index].isDirectory) { - // model.openDirectory(entries[index].path, isLocal: isLocal); + // openDirectory(entries[index].path, isLocal: isLocal); // breadCrumbScrollToEnd(isLocal); // } else { // // Perform file-related tasks. @@ -492,7 +491,7 @@ class _FileManagerPageState extends State final _locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; - final _searchTextObs = isLocal ? searchTextLocal : searchTextRemote; + final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; return Container( child: Column( children: [ @@ -579,7 +578,7 @@ class _FileManagerPageState extends State ], onChanged: (path) { if (path is String && path.isNotEmpty) { - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); } }) ], @@ -762,9 +761,11 @@ class _FileManagerPageState extends State for (var item in list) { path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); } - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); }), divider: Text("/").paddingSymmetric(horizontal: 4.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), ); } @@ -782,6 +783,16 @@ class _FileManagerPageState extends State 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, @@ -793,16 +804,23 @@ class _FileManagerPageState extends State controller: TextEditingController(text: model.getCurrentDir(isLocal).path), onSubmitted: (path) { - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); }, ); } onSearchText(String searchText, bool isLocal) { if (isLocal) { - searchTextLocal.value = searchText; + _searchTextLocal.value = searchText; } else { - searchTextRemote.value = searchText; + _searchTextRemote.value = searchText; } } + + openDirectory(String path, {bool isLocal = false}) { + model.openDirectory(path, isLocal: isLocal).then((_) { + print("scroll"); + breadCrumbScrollToEnd(isLocal); + }); + } } From 4bd5fe1509d6eebecc18425836b5fe9a847a0544 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:50:08 +0800 Subject: [PATCH 4/5] opt: entries empty fallback Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 104 ++++++++++-------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index a35ce4378..fc20d5277 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -163,28 +163,33 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - child: Obx( - () { - final searchText = - isLocal ? _searchTextLocal : _searchTextRemote; - final filteredEntries = entries.where((element) { - if (searchText.isEmpty) { - return true; - } else { - return element.name.contains(searchText.value); - } - }).toList(growable: false); - 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 + child: entries.isEmpty + ? Offstage() + : Obx( + () { + final searchText = + isLocal ? _searchTextLocal : _searchTextRemote; + 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, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon DataColumn( label: Text( translate("Name"), @@ -264,18 +269,20 @@ class _FileManagerPageState extends State .lastModified() .toString() .replaceAll(".000", "") + - " ", - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - DataCell(Text( - sizeStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - ]); - }).toList(), - ); + " ", + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray), + )), + ]); + }).toList(growable: false), + ); }, ), ), @@ -753,20 +760,21 @@ class _FileManagerPageState extends State } Widget buildBread(bool isLocal) { - final directory = model.getCurrentDir(isLocal); - print(directory.path); - return BreadCrumb( - items: getPathBreadCrumbItems(isLocal, (list) { - var path = ""; - for (var item in list) { - path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); - } - openDirectory(path, isLocal: isLocal); - }), - divider: Text("/").paddingSymmetric(horizontal: 4.0), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(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( From a001b15335e53a8ea9ef7aa2c44c44c6975f8778 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 13:28:48 +0800 Subject: [PATCH 5/5] feat: drop to send files to remote Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 489 +++++++++--------- flutter/lib/models/file_model.dart | 1 + flutter/pubspec.lock | 9 +- flutter/pubspec.yaml | 1 + 4 files changed, 268 insertions(+), 232 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index fc20d5277..0111e5f90 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,6 +1,7 @@ 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'; @@ -39,6 +40,8 @@ class _FileManagerPageState extends State final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); + final _dropMaskVisible = false.obs; + ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; } @@ -155,243 +158,248 @@ class _FileManagerPageState extends State 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: entries.isEmpty - ? Offstage() - : Obx( - () { - final searchText = - isLocal ? _searchTextLocal : _searchTextRemote; - 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, - 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(() {}); + 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 { + return element.name.contains(searchText.value); } - }, - 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(); + }).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 { - _remoteSelectedItems.clear(); - } - } else { - // Perform file-related tasks. - final _selectedItems = - getSelectedItem(isLocal); - if (_selectedItems.contains(entry)) { - _selectedItems.remove(entry); - } else { - _selectedItems.add(isLocal, entry); + getSelectedItem(isLocal).remove(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), - ); - }, + }, + 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) { - // 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(() {}); + // }, + // ), + // ); + // }, + // )) + ]), + ), ); } @@ -831,4 +839,23 @@ class _FileManagerPageState extends State 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 1c3960b50..74c2cd515 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -732,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