change file model

This commit is contained in:
csf 2022-03-11 01:28:13 +08:00
parent b9b8513423
commit 0305796ca3
4 changed files with 333 additions and 140 deletions

View File

@ -28,6 +28,7 @@ class App extends StatelessWidget {
ChangeNotifierProvider.value(value: FFI.imageModel), ChangeNotifierProvider.value(value: FFI.imageModel),
ChangeNotifierProvider.value(value: FFI.cursorModel), ChangeNotifierProvider.value(value: FFI.cursorModel),
ChangeNotifierProvider.value(value: FFI.canvasModel), ChangeNotifierProvider.value(value: FFI.canvasModel),
ChangeNotifierProvider.value(value: FFI.fileModel),
], ],
child: MaterialApp( child: MaterialApp(
navigatorKey: globalKey, navigatorKey: globalKey,

View File

@ -13,6 +13,146 @@ enum SortBy { name, type, date, size }
// FileLink = 5, // 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<String, dynamic> 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<String, dynamic> evt) {
_jobProgress.state = JobState.done;
// TODO
notifyListeners();
}
jobError(Map<String, dynamic> 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 { class FileDirectory {
List<Entry> entries = []; List<Entry> entries = [];
int id = 0; int id = 0;
@ -69,77 +209,21 @@ class Entry {
} }
} }
// TODO 使 enum JobState { none, inProgress, done, error }
class FileModel extends ChangeNotifier { class JobProgress {
var _jobCount = 0; JobState state = JobState.none;
var id = 0;
var fileNum = 0;
var speed = 0;
var finishedSize = 0;
SortBy _sortStyle = SortBy.name; clear() {
state = JobState.none;
SortBy get sortStyle => _sortStyle; id = 0;
fileNum = 0;
FileDirectory _currentLocalDir = FileDirectory(); speed = 0;
finishedSize = 0;
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();
} }
} }

View File

@ -136,6 +136,12 @@ class FfiModel with ChangeNotifier {
FFI.chatModel.receive(evt['text'] ?? ""); FFI.chatModel.receive(evt['text'] ?? "");
} else if (name == 'file_dir') { } else if (name == 'file_dir') {
FFI.fileModel.tryUpdateDir(evt['value'] ?? "",false); 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); if (pos != null) FFI.cursorModel.updateCursorPosition(pos);

View File

@ -4,12 +4,13 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.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 '../common.dart';
import '../models/model.dart'; import '../models/model.dart';
import '../widgets/dialog.dart'; import '../widgets/dialog.dart';
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key); FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id; final String id;
@ -19,33 +20,30 @@ class FileManagerPage extends StatefulWidget {
} }
class _FileManagerPageState extends State<FileManagerPage> { class _FileManagerPageState extends State<FileManagerPage> {
final _fileModel = FFI.fileModel; final model = FFI.fileModel;
final _selectedItems = SelectedItems();
Timer? _interval; Timer? _interval;
Timer? _timer; Timer? _timer;
var _reconnects = 1; var _reconnects = 1;
final _breadCrumbScroller = ScrollController();
var _isLocal = false;
var _selectMode = false;
final List<String> _selectedItems = []; // entry对象数组
@override @override
void initState() { void initState() {
super.initState(); super.initState();
showLoading(translate('Connecting...')); showLoading(translate('Connecting...'));
FFI.connect(widget.id, isFileTransfer: true); FFI.connect(widget.id, isFileTransfer: true);
Future.delayed(Duration(seconds: 1), () {
final res = FFI.getByName("read_dir", FFI.getByName("get_home_dir")); final res = FFI.getByName("read_dir", FFI.getByName("get_home_dir"));
debugPrint("read_dir local :$res"); debugPrint("read_dir local :$res");
_fileModel.tryUpdateDir(res, true); model.tryUpdateDir(res, true);
});
_interval = Timer.periodic(Duration(milliseconds: 30), _interval = Timer.periodic(Duration(milliseconds: 30),
(timer) => FFI.ffiModel.update(widget.id, context, handleMsgBox)); (timer) => FFI.ffiModel.update(widget.id, context, handleMsgBox));
} }
@override @override
void dispose() { void dispose() {
_fileModel.clear(); model.clear();
_interval?.cancel(); _interval?.cancel();
FFI.close(); FFI.close();
EasyLoading.dismiss(); EasyLoading.dismiss();
@ -53,9 +51,16 @@ class _FileManagerPageState extends State<FileManagerPage> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Consumer<FileModel>(builder: (_context, _model, _child) {
return ChangeNotifierProvider.value( return WillPopScope(
value: _fileModel, onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
goBack();
}
return false;
},
child: Scaffold( child: Scaffold(
backgroundColor: MyTheme.grayBg, backgroundColor: MyTheme.grayBg,
appBar: AppBar( appBar: AppBar(
@ -65,24 +70,29 @@ class _FileManagerPageState extends State<FileManagerPage> {
]), ]),
leadingWidth: 200, leadingWidth: 200,
centerTitle: true, centerTitle: true,
title: Text(translate(_isLocal ? "Local" : "Remote")), title: Text(translate(model.isLocal ? "Local" : "Remote")),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.change_circle), icon: Icon(Icons.change_circle),
onPressed: () => setState(() { onPressed: ()=> model.togglePage(),
_isLocal = !_isLocal;
}),
) )
], ],
), ),
body: body(), body: body(),
bottomSheet: bottomSheet(), bottomSheet: bottomSheet(),
)); ));
});
bool needShowCheckBox(){
if(!model.selectMode){
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
} }
Widget body() => Consumer<FileModel>(builder: (context, fileModel, _child) { Widget body() {
final fd = final isLocal = model.isLocal;
_isLocal ? fileModel.currentLocalDir : fileModel.currentRemoteDir; final fd = model.currentDir;
final entries = fd.entries; final entries = fd.entries;
return Column(children: [ return Column(children: [
headTools(), headTools(),
@ -96,55 +106,66 @@ class _FileManagerPageState extends State<FileManagerPage> {
// 使 bottomSheet // 使 bottomSheet
return listTail(); return listTail();
} }
final path = p.join(fd.path,entries[index].name); final path = Path.join(fd.path, entries[index].name);
var selected = false; var selected = false;
if (_selectMode) { if (model.selectMode) {
selected = _selectedItems.any((e) => e == path); selected = _selectedItems.contains(path);
} }
return Card( return Card(
child: ListTile( child: ListTile(
leading: entries[index].isFile leading: Icon(entries[index].isFile?Icons.feed_outlined:Icons.folder,
? Icon(Icons.feed_outlined) size: 40),
: Icon(Icons.folder),
title: Text(entries[index].name), title: Text(entries[index].name),
trailing: _selectMode selected: selected,
// subtitle: Text(entries[index].lastModified().toString()),
trailing: needShowCheckBox()
? Checkbox( ? Checkbox(
value: selected, value: selected,
onChanged: (v) { onChanged: (v) {
if (v == null) return; if (v == null) return;
if (v && !selected) { if (v && !selected) {
setState(() { _selectedItems.add(isLocal,path);
_selectedItems.add(path);
});
} else if (!v && selected) { } else if (!v && selected) {
setState(() {
_selectedItems.remove(path); _selectedItems.remove(path);
});
} }
setState(() {});
}) })
: null, : null,
onTap: () { onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
if (selected) {
_selectedItems.remove(path);
} else {
_selectedItems.add(isLocal,path);
}
setState(() {});
return;
}
if (entries[index].isDirectory) { if (entries[index].isDirectory) {
_fileModel.openDirectory(path,_isLocal); model.openDirectory(path);
breadCrumbScrollToEnd();
} else { } else {
// Perform file-related tasks. // Perform file-related tasks.
} }
}, },
onLongPress: () { onLongPress: () {
setState(() { _selectedItems.clear();
_selectedItems.clear(); model.toggleSelectMode();
_selectMode = !_selectMode; if (model.selectMode) {
}); _selectedItems.add(isLocal,path);
}
setState(() {});
}, },
), ),
); );
}, },
)) ))
]); ]);
}); }
goBack() { goBack() {
_fileModel.goToParentDirectory(_isLocal); model.goToParentDirectory();
} }
void handleMsgBox(Map<String, dynamic> evt, String id) { void handleMsgBox(Map<String, dynamic> evt, String id) {
@ -176,6 +197,15 @@ class _FileManagerPageState extends State<FileManagerPage> {
} }
} }
breadCrumbScrollToEnd() {
Future.delayed(Duration(milliseconds: 200), () {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
});
}
Widget headTools() => Container( Widget headTools() => Container(
child: Row( child: Row(
children: [ children: [
@ -184,7 +214,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
items: getPathBreadCrumbItems(() => debugPrint("pressed home"), items: getPathBreadCrumbItems(() => debugPrint("pressed home"),
(e) => debugPrint("pressed url:$e")), (e) => debugPrint("pressed url:$e")),
divider: Icon(Icons.chevron_right), divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow(reverse: false), // TODO overflow: ScrollableOverflow(controller: _breadCrumbScroller),
)), )),
Row( Row(
children: [ children: [
@ -200,25 +230,31 @@ class _FileManagerPageState extends State<FileManagerPage> {
)) ))
.toList(); .toList();
}, },
onSelected: _fileModel.changeSortStyle), onSelected: model.changeSortStyle),
PopupMenuButton<String>( PopupMenuButton<String>(
icon: Icon(Icons.more_vert), icon: Icon(Icons.more_vert),
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [Icon(Icons.refresh), Text("刷新")],
Icon(Icons.refresh),
Text("刷新")
],
), ),
value: "refresh", value: "refresh",
),
PopupMenuItem(
child: Row(
children: [Icon(Icons.check), Text("多选")],
),
value: "select",
) )
]; ];
}, },
onSelected: (v){ onSelected: (v) {
if(v == "refresh"){ if (v == "refresh") {
_fileModel.refresh(_isLocal); model.refresh();
} else if (v == "select") {
_selectedItems.clear();
model.toggleSelectMode();
} }
}), }),
], ],
@ -239,8 +275,13 @@ class _FileManagerPageState extends State<FileManagerPage> {
return SizedBox(height: 100); return SizedBox(height: 100);
} }
///
/// localPage
/// otherPage
///
///
BottomSheet? bottomSheet() { BottomSheet? bottomSheet() {
if (!_selectMode) return null; if (!model.selectMode) return null;
return BottomSheet( return BottomSheet(
backgroundColor: MyTheme.grayBg, backgroundColor: MyTheme.grayBg,
enableDrag: false, enableDrag: false,
@ -248,6 +289,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
debugPrint("BottomSheet close"); debugPrint("BottomSheet close");
}, },
builder: (context) { builder: (context) {
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
return Container( return Container(
height: 65, height: 65,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
@ -259,35 +301,50 @@ class _FileManagerPageState extends State<FileManagerPage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// bottomSheet类框架
Row( Row(
children: [ children: [
Icon(Icons.check), CircularProgressIndicator(),
SizedBox(width: 5), isOtherPage?Icon(Icons.input):Icon(Icons.check),
Text( SizedBox(width: 16),
"已选择 ${_selectedItems.length}", Column(
style: TextStyle(fontSize: 18), 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( Row(
children: [ children: [
IconButton( (_selectedItems.length>0 && isOtherPage)? IconButton(
icon: Icon(Icons.paste), icon: Icon(Icons.paste),
onPressed: () { onPressed:() {
debugPrint("paste"); 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), icon: Icon(Icons.delete_forever),
onPressed: () {}, onPressed: () {},
), ),
IconButton( IconButton(
icon: Icon(Icons.cancel_outlined), icon: Icon(Icons.cancel_outlined),
onPressed: () { onPressed: () {
setState(() { model.toggleSelectMode();
_selectMode = false;
});
}, },
), ),
], ],
@ -301,10 +358,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
List<BreadCrumbItem> getPathBreadCrumbItems( List<BreadCrumbItem> getPathBreadCrumbItems(
void Function() onHome, void Function(String) onPressed) { void Function() onHome, void Function(String) onPressed) {
final path = _isLocal final path = model.currentDir.path;
? _fileModel.currentLocalDir.path final list = path.trim().split('/'); // TODO use Path
: _fileModel.currentRemoteDir.path;
final list = path.trim().split('/');
list.remove(""); list.remove("");
final breadCrumbList = [ final breadCrumbList = [
BreadCrumbItem( BreadCrumbItem(
@ -321,5 +376,52 @@ class _FileManagerPageState extends State<FileManagerPage> {
onPressed: () => onPressed(e))))); onPressed: () => onPressed(e)))));
return breadCrumbList; return breadCrumbList;
} }
}
class SelectedItems {
bool? _isLocal;
final List<String> _items = [];
List<String> 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;
}
} }