diff --git a/lib/models/file_model.dart b/lib/models/file_model.dart index e374dd362..03c94160d 100644 --- a/lib/models/file_model.dart +++ b/lib/models/file_model.dart @@ -2,10 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:file_manager/enums/sort_by.dart'; import 'package:flutter/material.dart'; import 'model.dart'; +// BIG TODO remove file manager ! + // enum FileType { // Dir = 0, @@ -17,23 +20,28 @@ import 'model.dart'; class FileDirectory { // List entries = []; - List entries = []; + List entries = []; int id = 0; String path = ""; FileDirectory(); - FileDirectory.fromJson(Map json) { + FileDirectory.fromJson(Map json,SortBy sort) { id = json['id']; path = json['path']; if (json['entries'] != null) { - entries = []; + entries = []; json['entries'].forEach((v) { - entries.add(new Entry.fromJson(v).toFileSystemEntity(path)); + entries.add(new Entry.fromJson(v).toRemoteFileSystemEntity(path)); }); + entries = _sortRemoteEntitiesList(entries, sort); } } + changeSortStyle(SortBy sort){ + entries = _sortRemoteEntitiesList(entries, sort); + } + // Map toJson() { // final Map data = new Map(); // data['entries'] = this.entries.map((v) => v.toJson()).toList(); @@ -42,7 +50,7 @@ class FileDirectory { // return data; // } - clear(){ + clear() { entries = []; id = 0; path = ""; @@ -64,13 +72,8 @@ class Entry { size = json['size']; } - FileSystemEntity toFileSystemEntity(String parentPath){ - // is dir - if(entryType<=3){ - return RemoteDir("$parentPath/$name"); - }else { - return RemoteFile("$parentPath/$name",modifiedTime,size); - } + RemoteFileSystemEntity toRemoteFileSystemEntity(String parentPath) { + return RemoteFileSystemEntity.from("$parentPath/$name", entryType, modifiedTime, size); } Map toJson() { @@ -83,36 +86,43 @@ class Entry { } } - // TODO 使用工厂单例模式 -class RemoteFileModel extends ChangeNotifier{ +class RemoteFileModel extends ChangeNotifier { + + SortBy _sortStyle = SortBy.name; + SortBy get sortStyle => _sortStyle; FileDirectory _currentRemoteDir = FileDirectory(); - FileDirectory get currentRemoteDir => _currentRemoteDir; - tryUpdateRemoteDir(String fd){ + tryUpdateRemoteDir(String fd) { debugPrint("tryUpdateRemoteDir:$fd"); - try{ - final fileDir = FileDirectory.fromJson(jsonDecode(fd)); + try { + final fileDir = FileDirectory.fromJson(jsonDecode(fd),_sortStyle); _currentRemoteDir = fileDir; debugPrint("_currentRemoteDir:${_currentRemoteDir.path}"); notifyListeners(); - }catch(e){ + } catch (e) { debugPrint("tryUpdateRemoteDir fail:$fd"); } } - goToParentDirectory(){ + goToParentDirectory() { var parentPath = ""; - if(_currentRemoteDir.path == ""){ + if (_currentRemoteDir.path == "") { parentPath = ""; - }else{ + } else { parentPath = Directory(_currentRemoteDir.path).parent.path; } FFI.setByName("read_remote_dir", parentPath); } + + changeSortStyle(SortBy sort){ + _currentRemoteDir.changeSortStyle(sort); + notifyListeners(); + } + @override void dispose() { _currentRemoteDir.clear(); @@ -120,20 +130,88 @@ class RemoteFileModel extends ChangeNotifier{ } } +// int entryType = 4; +// int modifiedTime = 0; +// String name = ""; +// int size = 0; -class RemoteDir extends FileSystemEntity implements Directory{ - - // int entryType = 4; - // int modifiedTime = 0; - // String name = ""; - // int size = 0; - - +class RemoteFileSystemEntity extends FileSystemEntity { + int entryType; + int modifiedTime; String path; + int size; - RemoteDir(this.path); + RemoteFileSystemEntity(this.path, + this.entryType, + this.modifiedTime, + this.size); + // 工厂模式 自动输出两个类型 + factory RemoteFileSystemEntity.from( + String path, + int entryType, + int modifiedTime, + int size + ) { + if (entryType > 3) { + return RemoteFile(path, + entryType, + modifiedTime, + size); + } else { + return RemoteFile(path, + entryType, + modifiedTime, + size); + } + } + + DateTime lastModifiedSync() { + return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000); + } + + int lengthSync() { + return size; + } + + bool isFile(){ + return entryType > 3; + } + + @override + // TODO: implement absolute + FileSystemEntity get absolute => throw UnimplementedError(); + + @override + Future exists() { + // TODO: implement exists + throw UnimplementedError(); + } + + @override + bool existsSync() { + // TODO: implement existsSync + throw UnimplementedError(); + } + + @override + Future rename(String newPath) { + // TODO: implement rename + throw UnimplementedError(); + } + + @override + FileSystemEntity renameSync(String newPath) { + // TODO: implement renameSync + throw UnimplementedError(); + } +} + +class RemoteDir extends RemoteFileSystemEntity implements Directory { + RemoteDir(path, entryType, modifiedTime, size) + : super(path, entryType, modifiedTime, size); + @override // TODO: implement absolute Directory get absolute => throw UnimplementedError(); @@ -174,13 +252,15 @@ class RemoteDir extends FileSystemEntity implements Directory{ } @override - Stream list({bool recursive = false, bool followLinks = true}) { + Stream list( + {bool recursive = false, bool followLinks = true}) { // TODO: implement list throw UnimplementedError(); } @override - List listSync({bool recursive = false, bool followLinks = true}) { + List listSync( + {bool recursive = false, bool followLinks = true}) { // TODO: implement listSync throw UnimplementedError(); } @@ -196,32 +276,12 @@ class RemoteDir extends FileSystemEntity implements Directory{ // TODO: implement renameSync throw UnimplementedError(); } - } -class RemoteFile extends FileSystemEntity implements File { +class RemoteFile extends RemoteFileSystemEntity implements File { - // int entryType = 4; - // int modifiedTime = 0; - // String name = ""; - // int size = 0; - - RemoteFile(this.path,this.modifiedTime,this.size); - - var path; - var modifiedTime; - var size; - - - @override - DateTime lastModifiedSync() { - return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000); - } - - @override - int lengthSync() { - return size; - } + RemoteFile(path, entryType, modifiedTime, size) + : super(path, entryType, modifiedTime, size); // *************************** @@ -379,29 +439,120 @@ class RemoteFile extends FileSystemEntity implements File { } @override - Future writeAsBytes(List bytes, {FileMode mode = FileMode.write, bool flush = false}) { + Future writeAsBytes(List bytes, + {FileMode mode = FileMode.write, bool flush = false}) { // TODO: implement writeAsBytes throw UnimplementedError(); } @override - void writeAsBytesSync(List bytes, {FileMode mode = FileMode.write, bool flush = false}) { + void writeAsBytesSync(List bytes, + {FileMode mode = FileMode.write, bool flush = false}) { // TODO: implement writeAsBytesSync } @override - Future writeAsString(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) { + Future writeAsString(String contents, + {FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false}) { // TODO: implement writeAsString throw UnimplementedError(); } @override - void writeAsStringSync(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) { + void writeAsStringSync(String contents, + {FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false}) { // TODO: implement writeAsStringSync } @override // TODO: implement absolute File get absolute => throw UnimplementedError(); - -} \ No newline at end of file +} + + +class _PathStat { + final String path; + final DateTime dateTime; + _PathStat(this.path, this.dateTime); +} + +// code from file_manager pkg after edit +List _sortRemoteEntitiesList( + List list, SortBy sortType) { + if (sortType == SortBy.name) { + // making list of only folders. + final dirs = list.where((element) => element is Directory).toList(); + // sorting folder list by name. + dirs.sort((a, b) => a.path.toLowerCase().compareTo(b.path.toLowerCase())); + + // making list of only flies. + final files = list.where((element) => element is File).toList(); + // sorting files list by name. + files.sort((a, b) => a.path.toLowerCase().compareTo(b.path.toLowerCase())); + + // first folders will go to list (if available) then files will go to list. + return [...dirs, ...files]; + } else if (sortType == SortBy.date) { + // making the list of Path & DateTime + List<_PathStat> _pathStat = []; + for (RemoteFileSystemEntity e in list) { + _pathStat.add(_PathStat(e.path, e.lastModifiedSync())); + } + + // sort _pathStat according to date + _pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime)); + + // sorting [list] accroding to [_pathStat] + list.sort((a, b) => _pathStat + .indexWhere((element) => element.path == a.path) + .compareTo(_pathStat.indexWhere((element) => element.path == b.path))); + return list; + } else if (sortType == SortBy.type) { + // making list of only folders. + final dirs = list.where((element) => element is Directory).toList(); + + // sorting folders by name. + dirs.sort((a, b) => a.path.toLowerCase().compareTo(b.path.toLowerCase())); + + // making the list of files + final files = list.where((element) => element is File).toList(); + + // sorting files list by extension. + files.sort((a, b) => a.path + .toLowerCase() + .split('.') + .last + .compareTo(b.path.toLowerCase().split('.').last)); + return [...dirs, ...files]; + } else if (sortType == SortBy.size) { + // create list of path and size + Map _sizeMap = {}; + for (RemoteFileSystemEntity e in list) { + _sizeMap[e.path] = e.lengthSync(); + } + + // making list of only folders. + final dirs = list.where((element) => element is Directory).toList(); + // sorting folder list by name. + dirs.sort((a, b) => a.path.toLowerCase().compareTo(b.path.toLowerCase())); + + // making list of only flies. + final files = list.where((element) => element is File).toList(); + + // creating sorted list of [_sizeMapList] by size. + final List> _sizeMapList = _sizeMap.entries.toList(); + _sizeMapList.sort((b, a) => a.value.compareTo(b.value)); + + // sort [list] according to [_sizeMapList] + files.sort((a, b) => _sizeMapList + .indexWhere((element) => element.key == a.path) + .compareTo( + _sizeMapList.indexWhere((element) => element.key == b.path))); + return [...dirs, ...files]; + } + return []; +} diff --git a/lib/pages/file_manager_page.dart b/lib/pages/file_manager_page.dart index 925b54910..d0a138a80 100644 --- a/lib/pages/file_manager_page.dart +++ b/lib/pages/file_manager_page.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:file_manager/file_manager.dart'; import 'package:flutter/material.dart'; 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 '../common.dart'; import '../models/model.dart'; @@ -24,6 +26,10 @@ class _FileManagerPageState extends State { Timer? _timer; var _reconnects = 1; var _isLocal = false; + var _selectMode = false; + final List _selectedItems = []; + + // final _breadCrumbScrollController = ScrollController(); @override void initState() { @@ -36,8 +42,8 @@ class _FileManagerPageState extends State { @override void dispose() { - _localFileModel.dispose(); _remoteFileModel.dispose(); + _localFileModel.dispose(); _interval?.cancel(); FFI.close(); EasyLoading.dismiss(); @@ -49,6 +55,7 @@ class _FileManagerPageState extends State { return ChangeNotifierProvider.value( value: _remoteFileModel, child: Scaffold( + backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack), @@ -65,43 +72,89 @@ class _FileManagerPageState extends State { })), ], ), - body: - Consumer(builder: (context, remoteModel, child) { - return FileManager( - controller: _localFileModel, - builder: (context, localSnapshot) { - final snapshot = _isLocal - ? localSnapshot - : remoteModel.currentRemoteDir.entries; - return ListView.builder( - itemCount: snapshot.length, - itemBuilder: (context, index) { - return Card( - child: ListTile( - leading: FileManager.isFile(snapshot[index]) - ? Icon(Icons.feed_outlined) - : Icon(Icons.folder), - title: Text(FileManager.basename(snapshot[index])), - onTap: () { - if (FileManager.isDirectory(snapshot[index])) { - _isLocal - ? _localFileModel - .openDirectory(snapshot[index]) - : readRemoteDir( - snapshot[index].path); // open directory - } else { - // Perform file-related tasks. - } - }, - ), - ); - }, - ); - }); - }), + body: body(), + bottomSheet: bottomSheet(), )); } + Widget body() => Consumer( + builder: (context, remoteModel, _child) => FileManager( + controller: _localFileModel, + emptyFolder: emptyPage(), + builder: (context, localSnapshot) { + final snapshot = + _isLocal ? localSnapshot : remoteModel.currentRemoteDir.entries; + return Column(children: [ + headTools(), + Expanded( + child: ListView.builder( + itemCount: snapshot.length + 1, + itemBuilder: (context, index) { + if (index >= snapshot.length) { + // 添加尾部信息 文件统计信息等 + // 添加快速返回上部 + // 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多 + return listTail(); + } + + var isFile = false; + if (_isLocal){ + isFile = FileManager.isFile(snapshot[index]); + }else { + isFile = (snapshot[index] as RemoteFileSystemEntity).isFile(); + } + + final path = snapshot[index].path; + var selected = false; + if (_selectMode) { + selected = _selectedItems.any((e) => e == path); + } + return Card( + child: ListTile( + leading: isFile + ? Icon(Icons.feed_outlined) + : Icon(Icons.folder), + title: Text(FileManager.basename(snapshot[index])), + trailing: _selectMode + ? Checkbox( + value: selected, + onChanged: (v) { + if (v == null) return; + if (v && !selected) { + setState(() { + _selectedItems.add(path); + }); + } else if (!v && selected) { + setState(() { + _selectedItems.remove(path); + }); + } + }) + : null, + onTap: () { + if (!isFile) { + if (_isLocal) { + _localFileModel.openDirectory(snapshot[index]); + } else { + readRemoteDir(path); + } + } else { + // Perform file-related tasks. + } + }, + onLongPress: () { + setState(() { + _selectedItems.clear(); + _selectMode = !_selectMode; + }); + }, + ), + ); + }, + )) + ]); + })); + goBack() { if (_isLocal) { _localFileModel.goToParentDirectory(); @@ -142,4 +195,144 @@ class _FileManagerPageState extends State { _reconnects = 1; } } + + 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(reverse: false), // TODO 计算容器宽度判断 + )), + Row( + children: [ + // 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)), + value: e, + )).toList(); + }, + onSelected: changeSortStyle), + IconButton(onPressed: () {}, icon: Icon(Icons.more_vert)), + ], + ) + ], + )); + + changeSortStyle(SortBy sort){ + if(_isLocal){ + _localFileModel.sortedBy = sort; + }else{ + _remoteFileModel.changeSortStyle(sort); + } + } + + Widget emptyPage() { + return Column( + children: [ + headTools(), + Expanded(child: Center(child: Text("Empty Directory"))) + ], + ); + } + + Widget listTail() { + return SizedBox(height: 100); + } + + BottomSheet? bottomSheet() { + if (!_selectMode) return null; + return BottomSheet( + backgroundColor: MyTheme.grayBg, + enableDrag: false, + onClosing: () { + debugPrint("BottomSheet close"); + }, + builder: (context) { + return Container( + height: 65, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: MyTheme.accent50, + borderRadius: BorderRadius.vertical(top: Radius.circular(10))), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.check), + SizedBox(width: 5), + Text( + "已选择 ${_selectedItems.length}", + style: TextStyle(fontSize: 18), + ), + ], + ), + Row( + children: [ + IconButton( + icon: Icon(Icons.paste), + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.delete_forever), + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.cancel_outlined), + onPressed: () { + setState(() { + _selectMode = false; + }); + }, + ), + ], + ) + ], + ), + ), + ); + }); + } + + List getPathBreadCrumbItems( + void Function() onHome, void Function(String) onPressed) { + final path = _isLocal + ? _localFileModel.getCurrentPath + : _remoteFileModel.currentRemoteDir.path; + final list = path.trim().split('/'); + list.remove(""); + final breadCrumbList = [ + BreadCrumbItem( + content: IconButton( + icon: Icon(Icons.home_filled), + onPressed: onHome, + )) + ]; + breadCrumbList.addAll(list.map((e) => BreadCrumbItem( + content: TextButton( + child: Text(e), + style: + ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(e))))); + return breadCrumbList; + } + +// // NOT GOOD +// void breadCrumbToLast() { +// try { +// _breadCrumbScrollController.animateTo( +// _breadCrumbScrollController.position.maxScrollExtent, +// curve: Curves.easeOut, +// duration: const Duration(milliseconds: 300), +// ); +// } catch (e) {} +// } } diff --git a/pubspec.yaml b/pubspec.yaml index 70138f856..22a12bd7c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 file_manager: ^1.0.0 + flutter_breadcrumb: ^1.0.1 dev_dependencies: flutter_launcher_icons: ^0.9.1