From 281acf7474b120cd3899c6f359c17f0cc3888cb2 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 16 Mar 2022 15:33:00 +0800 Subject: [PATCH] full remove action & create folder action --- lib/common.dart | 8 +- lib/models/file_model.dart | 376 ++++++++++++++++++++++++++++--- lib/models/model.dart | 45 ++-- lib/pages/file_manager_page.dart | 358 ++++++++++++++++------------- 4 files changed, 575 insertions(+), 212 deletions(-) diff --git a/lib/common.dart b/lib/common.dart index 75dd6efb6..de5e18a72 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -45,14 +45,14 @@ backToHome() { } typedef DialogBuilder = CustomAlertDialog Function( - StateSetter setState, VoidCallback close); + StateSetter setState, Function([dynamic]) close); class DialogManager { static BuildContext? _dialogContext; - static void reset() { + static void reset([result]) { if (_dialogContext != null) { - Navigator.pop(_dialogContext!); + Navigator.pop(_dialogContext!,result); } _dialogContext = null; } @@ -76,7 +76,7 @@ class DialogManager { builder: (context) { DialogManager.register(context); return StatefulBuilder( - builder: (_, setState) => builder(setState, DialogManager.reset)); + builder: (_, setState) => builder(setState,DialogManager.reset)); }); DialogManager.drop(); return res; diff --git a/lib/models/file_model.dart b/lib/models/file_model.dart index 6478ef588..f8fa8d3b8 100644 --- a/lib/models/file_model.dart +++ b/lib/models/file_model.dart @@ -1,4 +1,7 @@ +import 'dart:async'; import 'dart:convert'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/pages/file_manager_page.dart'; import 'package:path/path.dart' as p; import 'package:flutter/material.dart'; @@ -16,18 +19,19 @@ enum SortBy { name, type, date, size } // FileLink = 5, // } +class RemoveCompleter {} + typedef OnJobStateChange = void Function(JobState state, JobProgress jp); -// TODO 每个fd设置操作系统属性,不同的操作系统 有不同的文件连字符 封装各类Path功能 - class FileModel extends ChangeNotifier { + // TODO 添加 dispose 退出页面的时候清理数据以及尚未完成的任务和对话框 var _isLocal = false; var _selectMode = false; /// 每一个选择的文件或文件夹占用一个 _jobId,file_num是文件夹中的单独文件id /// 如 /// 发送单独一个文件 file_num = 0; - /// 发送一个文件夹,若文件夹下有3个文件 file_num = 2; + /// 发送一个文件夹,若文件夹下有3个文件 最后一个文件的 file_num = 2; var _jobId = 0; var _jobProgress = JobProgress(); // from rust update @@ -54,6 +58,10 @@ class FileModel extends ChangeNotifier { FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; + final _fileFetcher = FileFetcher(); + + final _jobResultListener = JobResultListener>(); + toggleSelectMode() { _selectMode = !_selectMode; notifyListeners(); @@ -78,14 +86,29 @@ class FileModel extends ChangeNotifier { } } + receiveFileDir(Map evt) { + _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); + } + + // job 类型 复制结束 删除结束 jobDone(Map evt) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; _jobProgress.state = JobState.done; refresh(); - notifyListeners(); } jobError(Map evt) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + // TODO + _selectMode = false; _jobProgress.clear(); _jobProgress.state = JobState.error; notifyListeners(); @@ -96,30 +119,28 @@ class FileModel extends ChangeNotifier { notifyListeners(); } - tryUpdateDir(String fd, bool isLocal) { - try { - final fileDir = FileDirectory.fromJson(jsonDecode(fd), _sortStyle); - if (isLocal) { - _currentLocalDir = fileDir; - } else { - _currentRemoteDir = fileDir; - } - notifyListeners(); // TODO use too early, error occur:setState() or markNeedsBuild() called during build. - } catch (e) { - debugPrint("Failed to tryUpdateDir :$fd"); - } + onReady() { + openDirectory(FFI.getByName("get_home_dir"), isLocal: true); + openDirectory(FFI.ffiModel.pi.homeDir, isLocal: false); } refresh() { openDirectory(_isLocal ? _currentLocalDir.path : _currentRemoteDir.path); } - openDirectory(String path) { - if (_isLocal) { - final res = FFI.getByName("read_dir", path); - tryUpdateDir(res, true); - } else { - FFI.setByName("read_remote_dir", path); + openDirectory(String path, {bool? isLocal}) async { + isLocal = isLocal ?? _isLocal; + try { + final fd = await _fileFetcher.fetchDirectory(path, isLocal); + fd.changeSortStyle(_sortStyle); + if (isLocal) { + _currentLocalDir = fd; + } else { + _currentRemoteDir = fd; + } + notifyListeners(); + } catch (e) { + debugPrint("Failed to openDirectory :$e"); } } @@ -149,29 +170,156 @@ class FileModel extends ChangeNotifier { }); } - removeAction(SelectedItems items) { + bool removeCheckboxRemember = false; + + removeAction(SelectedItems items) async { + removeCheckboxRemember = false; if (items.isLocal == null) { debugPrint("Failed to removeFile ,wrong path state"); return; } - items.items.forEach((entry) { + await Future.forEach(items.items, (Entry item) async { _jobId++; - if (entry.isFile) { - // TODO dir - final msg = { - "id": _jobId.toString(), - "path": entry.path, - "file_num": "0", - "is_remote": (!(items.isLocal!)).toString() - }; - debugPrint("remove :$msg"); - FFI.setByName("remove_file", jsonEncode(msg)); - // items.remove(entry); + var title = ""; + var content = ""; + late final List entries; + if (item.isFile) { + title = "是否永久删除文件"; + content = "${item.name}"; + entries = [item]; + } else if (item.isDirectory) { + title = "这不是一个空文件夹"; + showLoading("正在读取..."); + final fd = await _fileFetcher.fetchDirectoryRecursive( + _jobId, item.path, items.isLocal!); + EasyLoading.dismiss(); + // 空文件夹 + if(fd.entries.isEmpty){ + final confirm = await showRemoveDialog("是否删除空文件夹",item.name,false); + if(confirm == true){ + sendRemoveEmptyDir(item.path, 0, items.isLocal!); + } + return; + } + + debugPrint("removeDirAllIntent res:${fd.id}"); + entries = fd.entries; + } else { + debugPrint("none : ${item.toString()}"); + entries = []; + } + + for (var i = 0; i < entries.length; i++) { + final dirShow = item.isDirectory?"是否删除文件夹下的文件?\n":""; + final count = entries.length>1?"第 ${i + 1}/${entries.length} 项":""; + content = dirShow + "$count \n${entries[i].path}"; + final confirm = await showRemoveDialog(title,content,item.isDirectory); + debugPrint("已选择:$confirm"); + try { + if (confirm == true) { + sendRemoveFile(entries[i].path, i, items.isLocal!); + final res = await _jobResultListener.start(); + debugPrint("remove got res ${res.toString()}"); + // handle remove res; + if (item.isDirectory && + res['file_num'] == (entries.length - 1).toString()) { + sendRemoveEmptyDir(item.path, i, items.isLocal!); + } + } + if (removeCheckboxRemember) { + if (confirm == true) { + for (var j = i + 1; j < entries.length; j++) { + sendRemoveFile(entries[j].path, j, items.isLocal!); + final res = await _jobResultListener.start(); + debugPrint("remove got res ${res.toString()}"); + if (item.isDirectory && + res['file_num'] == (entries.length - 1).toString()) { + sendRemoveEmptyDir(item.path, i, items.isLocal!); + } + } + } + break; + } + } catch (e) {} } }); + refresh(); } - createDir(String path) {} + Future showRemoveDialog(String title,String content,bool showCheckbox) async { + return await DialogManager.show( + (setState, Function(bool v) close) => CustomAlertDialog( + title: Row( + children: [ + Icon(Icons.warning, color: Colors.red), + SizedBox(width: 20), + Text(title) + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(content), + SizedBox(height: 5), + Text("此操作不可逆!",style: TextStyle(fontWeight: FontWeight.bold)), + showCheckbox? + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + "应用于文件夹下所有文件", + ), + value: removeCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => removeCheckboxRemember = v); + }, + ):SizedBox.shrink() + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(true), child: Text("Yes")), + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), child: Text("No")) + ])); + } + + sendRemoveFile(String path, int fileNum, bool isLocal) { + final msg = { + "id": _jobId.toString(), + "path": path, + "file_num": fileNum.toString(), + "is_remote": (!(isLocal)).toString() + }; + FFI.setByName("remove_file", jsonEncode(msg)); + } + + sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { + final msg = { + "id": _jobId.toString(), + "path": path, + "is_remote": (!isLocal).toString() + }; + FFI.setByName("remove_all_empty_dirs", jsonEncode(msg)); + } + + createDir(String path) { + _jobId ++; + final msg = { + "id": _jobId.toString(), + "path": path, + "is_remote": (!isLocal).toString() + }; + FFI.setByName("create_dir",jsonEncode(msg)); + } + + cancelJob(int id){ + + } changeSortStyle(SortBy sort) { _sortStyle = sort; @@ -186,6 +334,149 @@ class FileModel extends ChangeNotifier { } } +class JobResultListener { + Completer? _completer; + Timer? _timer; + int _timeoutSecond = 5; + + bool get isListening => _completer != null; + + clear() { + if (_completer != null) { + _timer?.cancel(); + _timer = null; + _completer!.completeError("Cancel manually"); + _completer = null; + return; + } + } + + Future start() { + if (_completer != null) return Future.error("Already start listen"); + _completer = Completer(); + _timer = Timer(Duration(seconds: _timeoutSecond), () { + if (!_completer!.isCompleted) { + _completer!.completeError("Time out"); + } + _completer = null; + }); + return _completer!.future; + } + + complete(T res) { + if (_completer != null) { + _timer?.cancel(); + _timer = null; + _completer!.complete(res); + _completer = null; + return; + } + } +} + +class FileFetcher { + // Map> localTasks = Map(); // now we only use read local dir sync + Map> remoteTasks = Map(); + Map> readRecursiveTasks = Map(); + + Future registerReadTask(bool isLocal, String path) { + // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later + final tasks = remoteTasks; // bypass now + if (tasks.containsKey(path)) { + throw "Failed to registerReadTask, already have same read job"; + } + final c = Completer(); + tasks[path] = c; + + Timer(Duration(seconds: 2), () { + tasks.remove(path); + if (c.isCompleted) return; // 计时器加入map + c.completeError("Failed to read dir,timeout"); + }); + return c.future; + } + + Future registerReadRecursiveTask(int id) { + final tasks = readRecursiveTasks; + if (tasks.containsKey(id)) { + throw "Failed to registerRemoveTask, already have same ReadRecursive job"; + } + final c = Completer(); + tasks[id] = c; + + Timer(Duration(seconds: 2), () { + tasks.remove(id); + if (c.isCompleted) return; // 计时器加入map + c.completeError("Failed to read dir,timeout"); + }); + return c.future; + } + + tryCompleteTask(String? msg, String? isLocalStr) { + debugPrint("tryCompleteTask : $msg"); + if (msg == null || isLocalStr == null) return; + late final isLocal; + late final tasks; + if (isLocalStr == "true") { + isLocal = true; + } else if (isLocalStr == "false") { + isLocal = false; + } else { + return; + } + try { + final fd = FileDirectory.fromJson(jsonDecode(msg)); + if (fd.id > 0) { + // fd.id > 0 is result for read recursive + // TODO later,will be better if every fetch use ID,so that there will only one task map for read and recursive read + tasks = readRecursiveTasks; + final completer = tasks.remove(fd.id); + completer?.complete(fd); + } else if (fd.path.isNotEmpty) { + // result for normal read dir + // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later + tasks = remoteTasks; // bypass now + final completer = tasks.remove(fd.path); + completer?.complete(fd); + } + } catch (e) { + debugPrint("tryCompleteJob err :$e"); + } + } + + Future fetchDirectory(String path, bool isLocal) async { + debugPrint("fetch :$path"); + try { + if (isLocal) { + final res = FFI.getByName("read_dir", path); + final fd = FileDirectory.fromJson(jsonDecode(res)); + return fd; + } else { + FFI.setByName("read_remote_dir", path); + return registerReadTask(isLocal, path); + } + } catch (e) { + return Future.error(e); + } + } + + Future fetchDirectoryRecursive( + int id, String path, bool isLocal) async { + debugPrint("fetchDirectoryRecursive id:$id , path:$path"); + try { + final msg = { + "id": id.toString(), + "path": path, + "is_remote": (!isLocal).toString() + }; + FFI.setByName("read_dir_recursive", jsonEncode(msg)); + return registerReadRecursiveTask(id); + } catch (e) { + return Future.error(e); + } + } +} + class FileDirectory { List entries = []; int id = 0; @@ -195,7 +486,7 @@ class FileDirectory { FileDirectory(); - FileDirectory.fromJson(Map json, SortBy sort) { + FileDirectory.fromJsonWithSort(Map json, SortBy sort) { id = json['id']; path = json['path']; if (json['entries'] != null) { @@ -207,6 +498,17 @@ class FileDirectory { } } + FileDirectory.fromJson(Map json) { + id = json['id']; + path = json['path']; + if (json['entries'] != null) { + entries = []; + json['entries'].forEach((v) { + entries.add(new Entry.fromJsonWithPath(v, path)); + }); + } + } + changeSortStyle(SortBy sort) { entries = _sortList(entries, sort); } diff --git a/lib/models/model.dart b/lib/models/model.dart index b930a1400..42f2c0c49 100644 --- a/lib/models/model.dart +++ b/lib/models/model.dart @@ -135,7 +135,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'chat') { FFI.chatModel.receive(evt['text'] ?? ""); } else if (name == 'file_dir') { - FFI.fileModel.tryUpdateDir(evt['value'] ?? "",false); + // FFI.fileModel.fileFetcher.tryCompleteTask(evt['value'],evt['is_local']); + FFI.fileModel.receiveFileDir(evt); } else if (name == 'job_progress'){ FFI.fileModel.tryUpdateJobProgress(evt); } else if (name == 'job_done'){ @@ -185,29 +186,36 @@ class FfiModel with ChangeNotifier { void handlePeerInfo(Map evt, BuildContext context) { EasyLoading.dismiss(); + DialogManager.reset(); _pi.version = evt['version']; _pi.username = evt['username']; _pi.hostname = evt['hostname']; _pi.platform = evt['platform']; _pi.sasEnabled = evt['sas_enabled'] == "true"; _pi.currentDisplay = int.parse(evt['current_display']); - List displays = json.decode(evt['displays']); - _pi.displays = []; - for (int i = 0; i < displays.length; ++i) { - Map d0 = displays[i]; - var d = Display(); - d.x = d0['x'].toDouble(); - d.y = d0['y'].toDouble(); - d.width = d0['width']; - d.height = d0['height']; - _pi.displays.add(d); - } - if (_pi.currentDisplay < _pi.displays.length) { - _display = _pi.displays[_pi.currentDisplay]; - } - if (displays.length > 0) { - showLoading(translate('Connected, waiting for image...')); - _waitForImage = true; + _pi.homeDir = evt['home_dir']; + + if(evt['is_file_transfer'] == "true"){ + FFI.fileModel.onReady(); + }else{ + _pi.displays = []; + List displays = json.decode(evt['displays']); + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + _pi.displays.add(d); + } + if (_pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + } + if (displays.length > 0) { + showLoading(translate('Connected, waiting for image...')); + _waitForImage = true; + } } notifyListeners(); } @@ -918,6 +926,7 @@ class PeerInfo { String username = ""; String hostname = ""; String platform = ""; + String homeDir = ""; bool sasEnabled = false; int currentDisplay = 0; List displays = []; diff --git a/lib/pages/file_manager_page.dart b/lib/pages/file_manager_page.dart index 6272d1ee0..6bfe47cea 100644 --- a/lib/pages/file_manager_page.dart +++ b/lib/pages/file_manager_page.dart @@ -32,12 +32,8 @@ class _FileManagerPageState extends State { showLoading(translate('Connecting...')); FFI.connect(widget.id, isFileTransfer: true); - final res = FFI.getByName("read_dir", FFI.getByName("get_home_dir")); - debugPrint("read_dir local :$res"); - model.tryUpdateDir(res, true); - _interval = Timer.periodic(Duration(milliseconds: 30), - (timer) => FFI.ffiModel.update(widget.id, context, handleMsgBox)); + (timer) => FFI.ffiModel.update(widget.id, context, handleMsgBox)); } @override @@ -98,45 +94,49 @@ class _FileManagerPageState extends State { headTools(), Expanded( child: ListView.builder( - itemCount: entries.length + 1, - itemBuilder: (context, index) { - if (index >= entries.length) { - // 添加尾部信息 文件统计信息等 - // 添加快速返回上部 - // 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多 - return listTail(); - } - var selected = false; - if (model.selectMode) { - selected = _selectedItems.contains(entries[index]); - } - var sizeStr = ""; - if(entries[index].isFile){ - final size = entries[index].size; - if(size< 1024){ - sizeStr += size.toString() + "B"; - }else if(size< 1024 * 1024){ - sizeStr += (size/1024).toStringAsFixed(2) + "kB"; - }else if(size < 1024 * 1024 * 1024){ - sizeStr += (size/1024/1024).toStringAsFixed(2) + "MB"; - }else if(size < 1024 * 1024 * 1024 * 1024){ - sizeStr += (size/1024/1024/1024).toStringAsFixed(2) + "GB"; - } - } - 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( + itemCount: entries.length + 1, + itemBuilder: (context, index) { + if (index >= entries.length) { + // 添加尾部信息 文件统计信息等 + // 添加快速返回上部 + // 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多 + return listTail(); + } + var selected = false; + if (model.selectMode) { + selected = _selectedItems.contains(entries[index]); + } + var sizeStr = ""; + if (entries[index].isFile) { + final size = entries[index].size; + if (size < 1024) { + sizeStr += size.toString() + "B"; + } else if (size < 1024 * 1024) { + sizeStr += (size / 1024).toStringAsFixed(2) + "kB"; + } else if (size < 1024 * 1024 * 1024) { + sizeStr += (size / 1024 / 1024).toStringAsFixed(2) + "MB"; + } else if (size < 1024 * 1024 * 1024 * 1024) { + sizeStr += (size / 1024 / 1024 / 1024).toStringAsFixed(2) + "GB"; + } + } + 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; @@ -147,37 +147,57 @@ class _FileManagerPageState extends State { } setState(() {}); }) - : null, - 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); - breadCrumbScrollToEnd(); - } else { - // Perform file-related tasks. - } - }, - onLongPress: () { - _selectedItems.clear(); - model.toggleSelectMode(); - if (model.selectMode) { - _selectedItems.add(isLocal, entries[index]); - } - setState(() {}); - }, - ), - ); - }, - )) + : PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text("删除"), + value: "delete", + ), + PopupMenuItem( + child: Text("详细信息"), + value: "delete", + enabled: false, + ) + ]; + }, + onSelected: (v) { + if (v == "delete") { + final items = SelectedItems(); + items.add(isLocal, entries[index]); + model.removeAction(items); + } + }), + 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); + breadCrumbScrollToEnd(); + } else { + // Perform file-related tasks. + } + }, + onLongPress: () { + _selectedItems.clear(); + model.toggleSelectMode(); + if (model.selectMode) { + _selectedItems.add(isLocal, entries[index]); + } + setState(() {}); + }, + ), + ); + }, + )) ]); } @@ -223,68 +243,101 @@ class _FileManagerPageState extends State { }); } - Widget headTools() => - Container( + Widget headTools() => Container( child: Row( + children: [ + Expanded( + child: BreadCrumb( + items: getPathBreadCrumbItems(() => debugPrint("pressed home"), + (e) => debugPrint("pressed url:$e")), + divider: Icon(Icons.chevron_right), + overflow: ScrollableOverflow(controller: _breadCrumbScroller), + )), + Row( children: [ - Expanded( - child: BreadCrumb( - items: getPathBreadCrumbItems(() => - debugPrint("pressed home"), - (e) => debugPrint("pressed url:$e")), - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow( - controller: _breadCrumbScroller), - )), - Row( - children: [ - // IconButton(onPressed: () {}, icon: Icon(Icons.sort)), - PopupMenuButton( - icon: Icon(Icons.sort), - itemBuilder: (context) { - return SortBy.values - .map((e) => - PopupMenuItem( + // IconButton(onPressed: () {}, icon: Icon(Icons.sort)), + PopupMenuButton( + icon: Icon(Icons.sort), + itemBuilder: (context) { + return SortBy.values + .map((e) => PopupMenuItem( child: - Text(translate(e - .toString() - .split(".") - .last)), + Text(translate(e.toString().split(".").last)), value: e, )) - .toList(); - }, - onSelected: model.changeSortStyle), - PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Row( - children: [Icon(Icons.refresh), Text("刷新")], - ), - value: "refresh", - ), - PopupMenuItem( - child: Row( - children: [Icon(Icons.check), Text("多选")], - ), - value: "select", - ) - ]; - }, - onSelected: (v) { - if (v == "refresh") { - model.refresh(); - } else if (v == "select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } - }), - ], - ) + .toList(); + }, + onSelected: model.changeSortStyle), + PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: [Icon(Icons.refresh), Text("刷新")], + ), + value: "refresh", + ), + PopupMenuItem( + child: Row( + children: [Icon(Icons.check), Text("多选")], + ), + value: "select", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.folder), + Text(translate("Create Folder")) + ], + ), + value: "folder", + ) + ]; + }, + onSelected: (v) { + if (v == "refresh") { + model.refresh(); + } else if (v == "select") { + _selectedItems.clear(); + model.toggleSelectMode(); + } else if (v == "folder") { + final name = TextEditingController(); + 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: () { + if (name.value.text.isNotEmpty) { + model.createDir(Path.join(model.currentDir.path,name.value.text)); + close(); + } + }, + child: Text(translate("OK"))), + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))) + ])); + } + }), ], - )); + ) + ], + )); Widget emptyPage() { return Column( @@ -319,7 +372,7 @@ class _FileManagerPageState extends State { IconButton( icon: Icon(Icons.delete_forever), onPressed: () { - if(_selectedItems.length>0){ + if (_selectedItems.length > 0) { model.removeAction(_selectedItems); } }, @@ -337,7 +390,6 @@ class _FileManagerPageState extends State { icon: Icon(Icons.paste), onPressed: () { model.toggleSelectMode(); - // TODO model.sendFiles(_selectedItems); }, ) @@ -350,21 +402,21 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: CircularProgressIndicator(), title: "正在发送文件...", - text: "速度: ${(model.jobProgress.speed / 1024).toStringAsFixed( - 2)} kb/s", + text: + "速度: ${(model.jobProgress.speed / 1024).toStringAsFixed(2)} kb/s", onCanceled: null, ); case JobState.done: return BottomSheetBody( leading: Icon(Icons.check), - title: "发送成功!", + title: "操作成功!", text: "", onCanceled: () => model.jobReset(), ); case JobState.error: return BottomSheetBody( leading: Icon(Icons.error), - title: "发送错误!", + title: "错误!", text: "", onCanceled: () => model.jobReset(), ); @@ -374,35 +426,35 @@ class _FileManagerPageState extends State { return null; } - List getPathBreadCrumbItems(void Function() onHome, - void Function(String) onPressed) { + List getPathBreadCrumbItems( + void Function() onHome, void Function(String) onPressed) { final path = model.currentDir.path; final list = Path.split(path); list.remove('/'); final breadCrumbList = [ BreadCrumbItem( content: IconButton( - icon: Icon(Icons.home_filled), - onPressed: onHome, - )) + icon: Icon(Icons.home_filled), + onPressed: onHome, + )) ]; - breadCrumbList.addAll(list.map((e) => - BreadCrumbItem( - content: TextButton( - child: Text(e), - style: + breadCrumbList.addAll(list.map((e) => BreadCrumbItem( + content: TextButton( + child: Text(e), + style: ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(e))))); + onPressed: () => onPressed(e))))); return breadCrumbList; } } class BottomSheetBody extends StatelessWidget { - BottomSheetBody({required this.leading, - required this.title, - required this.text, - this.onCanceled, - this.actions}); + BottomSheetBody( + {required this.leading, + required this.title, + required this.text, + this.onCanceled, + this.actions}); final Widget leading; final String title;