diff --git a/lib/main.dart b/lib/main.dart index 9b893c3af..66477062b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,7 @@ class App extends StatelessWidget { ChangeNotifierProvider.value(value: FFI.imageModel), ChangeNotifierProvider.value(value: FFI.cursorModel), ChangeNotifierProvider.value(value: FFI.canvasModel), + ChangeNotifierProvider.value(value: FFI.fileModel), ], child: MaterialApp( navigatorKey: globalKey, diff --git a/lib/models/file_model.dart b/lib/models/file_model.dart index bb6d4521d..af4de525c 100644 --- a/lib/models/file_model.dart +++ b/lib/models/file_model.dart @@ -13,6 +13,146 @@ enum SortBy { name, type, date, size } // FileLink = 5, // } +typedef OnJobStateChange = void Function(JobState state, JobProgress jp); + +// TODO 使用工厂单例模式 + +class FileModel extends ChangeNotifier { + var _isLocal = false; + var _selectMode = false; + + + var _jobId = 0; + + var _jobProgress = JobProgress(); // from rust update + + bool get isLocal => _isLocal; + + bool get selectMode => _selectMode; + + JobProgress get jobProgress => _jobProgress; + + JobState get jobState => _jobProgress.state; + + SortBy _sortStyle = SortBy.name; + + SortBy get sortStyle => _sortStyle; + + FileDirectory _currentLocalDir = FileDirectory(); + + FileDirectory get currentLocalDir => _currentLocalDir; + + FileDirectory _currentRemoteDir = FileDirectory(); + + FileDirectory get currentRemoteDir => _currentRemoteDir; + + FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; + + OnJobStateChange? _onJobStateChange; + + setOnJobStateChange(OnJobStateChange v) { + _onJobStateChange = v; + } + + toggleSelectMode() { + _selectMode = !_selectMode; + notifyListeners(); + } + + togglePage() { + _isLocal = !_isLocal; + notifyListeners(); + } + + tryUpdateJobProgress(Map evt) { + try { + int id = int.parse(evt['id']); + if (id == _jobId) { + _jobProgress.id = id; + _jobProgress.fileNum = int.parse(evt['file_num']); + _jobProgress.speed = int.parse(evt['speed']); + _jobProgress.finishedSize = int.parse(evt['finished_size']); + notifyListeners(); + } else { + debugPrint( + "Failed to updateJobProgress ,id != _jobId,id:$id,_jobId:$_jobId"); + } + } catch (e) { + debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); + } + } + + jobDone(Map evt) { + _jobProgress.state = JobState.done; + // TODO + + notifyListeners(); + } + + jobError(Map evt) { + // TODO + _jobProgress.clear(); + _jobProgress.state = JobState.error; + notifyListeners(); + } + + tryUpdateDir(String fd, bool isLocal) { + try { + final fileDir = FileDirectory.fromJson(jsonDecode(fd), _sortStyle); + if (isLocal) { + _currentLocalDir = fileDir; + } else { + _currentRemoteDir = fileDir; + } + notifyListeners(); + } catch (e) { + debugPrint("Failed to tryUpdateDir :$fd"); + } + } + + 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); + } + } + + goToParentDirectory() { + final fd = _isLocal ? _currentLocalDir : _currentRemoteDir; + openDirectory(fd.parent); + } + + sendFiles(String path, String to, bool showHidden, bool isRemote) { + _jobId++; + final msg = { + "id": _jobId.toString(), + "path": path, + "to": to, + "show_hidden": showHidden.toString(), + "is_remote": isRemote.toString() // isRemote 指path的位置而不是to的位置 + }; + FFI.setByName("send_files", jsonEncode(msg)); + } + + changeSortStyle(SortBy sort) { + _sortStyle = sort; + _currentLocalDir.changeSortStyle(sort); + _currentRemoteDir.changeSortStyle(sort); + notifyListeners(); + } + + void clear() { + _currentLocalDir.clear(); + _currentRemoteDir.clear(); + } +} + class FileDirectory { List entries = []; int id = 0; @@ -69,77 +209,21 @@ class Entry { } } -// TODO 使用工厂单例模式 +enum JobState { none, inProgress, done, error } -class FileModel extends ChangeNotifier { - var _jobCount = 0; +class JobProgress { + JobState state = JobState.none; + var id = 0; + var fileNum = 0; + var speed = 0; + var finishedSize = 0; - SortBy _sortStyle = SortBy.name; - - SortBy get sortStyle => _sortStyle; - - FileDirectory _currentLocalDir = FileDirectory(); - - FileDirectory get currentLocalDir => _currentLocalDir; - - FileDirectory _currentRemoteDir = FileDirectory(); - - FileDirectory get currentRemoteDir => _currentRemoteDir; - - tryUpdateDir(String fd, bool isLocal) { - try { - final fileDir = FileDirectory.fromJson(jsonDecode(fd), _sortStyle); - if (isLocal) { - _currentLocalDir = fileDir; - } else { - _currentRemoteDir = fileDir; - } - notifyListeners(); - } catch (e) { - debugPrint("tryUpdateDir fail:$fd"); - } - } - - refresh(bool isLocal){ - openDirectory(isLocal?_currentLocalDir.path:_currentRemoteDir.path,isLocal); - } - - openDirectory(String path, bool isLocal) { - if (isLocal) { - final res = FFI.getByName("read_dir", path); - tryUpdateDir(res, true); - } else { - FFI.setByName("read_remote_dir", path); - } - } - - goToParentDirectory(bool isLocal) { - final fd = isLocal ? _currentLocalDir : _currentRemoteDir; - openDirectory(fd.parent, isLocal); - } - - sendFiles(String path, String to, bool showHidden, bool isRemote) { - _jobCount++; - final msg = { - "id": _jobCount.toString(), - "path": path, - "to": to, - "show_hidden": showHidden.toString(), - "is_remote": isRemote.toString() // isRemote 指path的位置而不是to的位置 - }; - FFI.setByName("send_files",jsonEncode(msg)); - } - - changeSortStyle(SortBy sort) { - _sortStyle = sort; - _currentLocalDir.changeSortStyle(sort); - _currentRemoteDir.changeSortStyle(sort); - notifyListeners(); - } - - void clear() { - _currentLocalDir.clear(); - _currentRemoteDir.clear(); + clear() { + state = JobState.none; + id = 0; + fileNum = 0; + speed = 0; + finishedSize = 0; } } diff --git a/lib/models/model.dart b/lib/models/model.dart index af8c54e25..b930a1400 100644 --- a/lib/models/model.dart +++ b/lib/models/model.dart @@ -136,6 +136,12 @@ class FfiModel with ChangeNotifier { FFI.chatModel.receive(evt['text'] ?? ""); } else if (name == 'file_dir') { FFI.fileModel.tryUpdateDir(evt['value'] ?? "",false); + } else if (name == 'job_progress'){ + FFI.fileModel.tryUpdateJobProgress(evt); + } else if (name == 'job_done'){ + FFI.fileModel.jobDone(evt); + } else if (name == 'job_error'){ + FFI.fileModel.jobError(evt); } } if (pos != null) FFI.cursorModel.updateCursorPosition(pos); diff --git a/lib/pages/file_manager_page.dart b/lib/pages/file_manager_page.dart index dfd57936f..fe3704c5b 100644 --- a/lib/pages/file_manager_page.dart +++ b/lib/pages/file_manager_page.dart @@ -4,12 +4,13 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:provider/provider.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; -import 'package:path/path.dart' as p; +import 'package:path/path.dart' as Path; import '../common.dart'; import '../models/model.dart'; import '../widgets/dialog.dart'; + class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -19,33 +20,30 @@ class FileManagerPage extends StatefulWidget { } class _FileManagerPageState extends State { - final _fileModel = FFI.fileModel; + final model = FFI.fileModel; + final _selectedItems = SelectedItems(); Timer? _interval; Timer? _timer; var _reconnects = 1; - - var _isLocal = false; - var _selectMode = false; - final List _selectedItems = []; // 换成entry对象数组 - + final _breadCrumbScroller = ScrollController(); @override void initState() { super.initState(); showLoading(translate('Connecting...')); FFI.connect(widget.id, isFileTransfer: true); - Future.delayed(Duration(seconds: 1), () { - final res = FFI.getByName("read_dir", FFI.getByName("get_home_dir")); - debugPrint("read_dir local :$res"); - _fileModel.tryUpdateDir(res, 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)); } @override void dispose() { - _fileModel.clear(); + model.clear(); _interval?.cancel(); FFI.close(); EasyLoading.dismiss(); @@ -53,9 +51,16 @@ class _FileManagerPageState extends State { } @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: _fileModel, + Widget build(BuildContext context) => Consumer(builder: (_context, _model, _child) { + return WillPopScope( + onWillPop: () async { + if (model.selectMode) { + model.toggleSelectMode(); + } else { + goBack(); + } + return false; + }, child: Scaffold( backgroundColor: MyTheme.grayBg, appBar: AppBar( @@ -65,24 +70,29 @@ class _FileManagerPageState extends State { ]), leadingWidth: 200, centerTitle: true, - title: Text(translate(_isLocal ? "Local" : "Remote")), + title: Text(translate(model.isLocal ? "Local" : "Remote")), actions: [ IconButton( icon: Icon(Icons.change_circle), - onPressed: () => setState(() { - _isLocal = !_isLocal; - }), + onPressed: ()=> model.togglePage(), ) ], ), body: body(), bottomSheet: bottomSheet(), )); + }); + + bool needShowCheckBox(){ + if(!model.selectMode){ + return false; + } + return !_selectedItems.isOtherPage(model.isLocal); } - Widget body() => Consumer(builder: (context, fileModel, _child) { - final fd = - _isLocal ? fileModel.currentLocalDir : fileModel.currentRemoteDir; + Widget body() { + final isLocal = model.isLocal; + final fd = model.currentDir; final entries = fd.entries; return Column(children: [ headTools(), @@ -96,55 +106,66 @@ class _FileManagerPageState extends State { // 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多 return listTail(); } - final path = p.join(fd.path,entries[index].name); + final path = Path.join(fd.path, entries[index].name); var selected = false; - if (_selectMode) { - selected = _selectedItems.any((e) => e == path); + if (model.selectMode) { + selected = _selectedItems.contains(path); } return Card( child: ListTile( - leading: entries[index].isFile - ? Icon(Icons.feed_outlined) - : Icon(Icons.folder), + leading: Icon(entries[index].isFile?Icons.feed_outlined:Icons.folder, + size: 40), + title: Text(entries[index].name), - trailing: _selectMode + selected: selected, + // subtitle: Text(entries[index].lastModified().toString()), + trailing: needShowCheckBox() ? Checkbox( value: selected, onChanged: (v) { if (v == null) return; if (v && !selected) { - setState(() { - _selectedItems.add(path); - }); + _selectedItems.add(isLocal,path); } else if (!v && selected) { - setState(() { _selectedItems.remove(path); - }); } + setState(() {}); }) : null, onTap: () { + if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { + if (selected) { + _selectedItems.remove(path); + } else { + _selectedItems.add(isLocal,path); + } + setState(() {}); + return; + } if (entries[index].isDirectory) { - _fileModel.openDirectory(path,_isLocal); + model.openDirectory(path); + breadCrumbScrollToEnd(); } else { // Perform file-related tasks. } }, onLongPress: () { - setState(() { - _selectedItems.clear(); - _selectMode = !_selectMode; - }); + _selectedItems.clear(); + model.toggleSelectMode(); + if (model.selectMode) { + _selectedItems.add(isLocal,path); + } + setState(() {}); }, ), ); }, )) ]); - }); + } goBack() { - _fileModel.goToParentDirectory(_isLocal); + model.goToParentDirectory(); } void handleMsgBox(Map evt, String id) { @@ -176,6 +197,15 @@ class _FileManagerPageState extends State { } } + breadCrumbScrollToEnd() { + Future.delayed(Duration(milliseconds: 200), () { + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + }); + } + Widget headTools() => Container( child: Row( children: [ @@ -184,7 +214,7 @@ class _FileManagerPageState extends State { items: getPathBreadCrumbItems(() => debugPrint("pressed home"), (e) => debugPrint("pressed url:$e")), divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow(reverse: false), // TODO 计算容器宽度判断 + overflow: ScrollableOverflow(controller: _breadCrumbScroller), )), Row( children: [ @@ -200,25 +230,31 @@ class _FileManagerPageState extends State { )) .toList(); }, - onSelected: _fileModel.changeSortStyle), + onSelected: model.changeSortStyle), PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { return [ PopupMenuItem( child: Row( - children: [ - Icon(Icons.refresh), - Text("刷新") - ], + children: [Icon(Icons.refresh), Text("刷新")], ), value: "refresh", + ), + PopupMenuItem( + child: Row( + children: [Icon(Icons.check), Text("多选")], + ), + value: "select", ) ]; }, - onSelected: (v){ - if(v == "refresh"){ - _fileModel.refresh(_isLocal); + onSelected: (v) { + if (v == "refresh") { + model.refresh(); + } else if (v == "select") { + _selectedItems.clear(); + model.toggleSelectMode(); } }), ], @@ -239,8 +275,13 @@ class _FileManagerPageState extends State { return SizedBox(height: 100); } + /// 有几种状态 + /// 选择模式 localPage + /// 准备复制模式 otherPage + /// 正在复制模式 动态数字和显示速度 + /// 粘贴完成模式 BottomSheet? bottomSheet() { - if (!_selectMode) return null; + if (!model.selectMode) return null; return BottomSheet( backgroundColor: MyTheme.grayBg, enableDrag: false, @@ -248,6 +289,7 @@ class _FileManagerPageState extends State { debugPrint("BottomSheet close"); }, builder: (context) { + final isOtherPage = _selectedItems.isOtherPage(model.isLocal); return Container( height: 65, alignment: Alignment.centerLeft, @@ -259,35 +301,50 @@ class _FileManagerPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // 做一个bottomSheet类框架 不同状态下显示不同的内容 Row( children: [ - Icon(Icons.check), - SizedBox(width: 5), - Text( - "已选择 ${_selectedItems.length}", - style: TextStyle(fontSize: 18), - ), + CircularProgressIndicator(), + isOtherPage?Icon(Icons.input):Icon(Icons.check), + SizedBox(width: 16), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(isOtherPage?'粘贴到这里?':'已选择',style: TextStyle(fontSize: 18)), + Text("${_selectedItems.length} 个文件 [${model.isLocal?'本地':'远程'}]",style: TextStyle(fontSize: 14,color: MyTheme.grayBg)) + ], + ) ], ), Row( children: [ - IconButton( + (_selectedItems.length>0 && isOtherPage)? IconButton( icon: Icon(Icons.paste), - onPressed: () { + onPressed:() { debugPrint("paste"); - _fileModel.sendFiles(_selectedItems.first, _fileModel.currentRemoteDir.path+'/'+_selectedItems.first.split('/').last, false, false); + // TODO  + + + model.sendFiles( + _selectedItems.items.first, + model.currentRemoteDir.path + + '/' + + _selectedItems.items.first.split('/').last, + false, + false); + + // unused set callback + // _fileModel.set }, - ), - IconButton( + ):IconButton( icon: Icon(Icons.delete_forever), onPressed: () {}, ), IconButton( icon: Icon(Icons.cancel_outlined), onPressed: () { - setState(() { - _selectMode = false; - }); + model.toggleSelectMode(); }, ), ], @@ -301,10 +358,8 @@ class _FileManagerPageState extends State { List getPathBreadCrumbItems( void Function() onHome, void Function(String) onPressed) { - final path = _isLocal - ? _fileModel.currentLocalDir.path - : _fileModel.currentRemoteDir.path; - final list = path.trim().split('/'); + final path = model.currentDir.path; + final list = path.trim().split('/'); // TODO use Path list.remove(""); final breadCrumbList = [ BreadCrumbItem( @@ -321,5 +376,52 @@ class _FileManagerPageState extends State { onPressed: () => onPressed(e))))); return breadCrumbList; } - +} + + +class SelectedItems { + bool? _isLocal; + final List _items = []; + + List get items => _items; + + int get length => _items.length; + + // bool get isEmpty => _items.length == 0; + + add(bool isLocal, String path) { + if (_isLocal == null) { + _isLocal = isLocal; + } + if (_isLocal != null && _isLocal != isLocal) { + return; + } + if (!_items.contains(path)) { + _items.add(path); + } + } + + bool contains(String path) { + return _items.contains(path); + } + + remove(String path) { + _items.remove(path); + if (_items.length == 0) { + _isLocal = null; + } + } + + bool isOtherPage(bool currentIsLocal) { + if (_isLocal == null) { + return false; + } else { + return _isLocal != currentIsLocal; + } + } + + clear() { + _items.clear(); + _isLocal = null; + } }