fix send multi files;add file remove action

This commit is contained in:
csf 2022-03-12 21:42:05 +08:00
parent 0305796ca3
commit 3318fb0471
2 changed files with 350 additions and 233 deletions

View File

@ -1,6 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_hbb/pages/file_manager_page.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as Path;
import 'model.dart'; import 'model.dart';
enum SortBy { name, type, date, size } enum SortBy { name, type, date, size }
@ -15,13 +18,16 @@ enum SortBy { name, type, date, size }
typedef OnJobStateChange = void Function(JobState state, JobProgress jp); typedef OnJobStateChange = void Function(JobState state, JobProgress jp);
// TODO 使 // TODO fd设置操作系统属性 Path功能
class FileModel extends ChangeNotifier { class FileModel extends ChangeNotifier {
var _isLocal = false; var _isLocal = false;
var _selectMode = false; var _selectMode = false;
/// _jobIdfile_num是文件夹中的单独文件id
///
/// file_num = 0;
/// 3 file_num = 2;
var _jobId = 0; var _jobId = 0;
var _jobProgress = JobProgress(); // from rust update var _jobProgress = JobProgress(); // from rust update
@ -48,12 +54,6 @@ class FileModel extends ChangeNotifier {
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
OnJobStateChange? _onJobStateChange;
setOnJobStateChange(OnJobStateChange v) {
_onJobStateChange = v;
}
toggleSelectMode() { toggleSelectMode() {
_selectMode = !_selectMode; _selectMode = !_selectMode;
notifyListeners(); notifyListeners();
@ -67,16 +67,12 @@ class FileModel extends ChangeNotifier {
tryUpdateJobProgress(Map<String, dynamic> evt) { tryUpdateJobProgress(Map<String, dynamic> evt) {
try { try {
int id = int.parse(evt['id']); int id = int.parse(evt['id']);
if (id == _jobId) { _jobProgress.id = id;
_jobProgress.id = id; _jobProgress.fileNum = int.parse(evt['file_num']);
_jobProgress.fileNum = int.parse(evt['file_num']); _jobProgress.speed = double.parse(evt['speed']);
_jobProgress.speed = int.parse(evt['speed']); _jobProgress.finishedSize = int.parse(evt['finished_size']);
_jobProgress.finishedSize = int.parse(evt['finished_size']); debugPrint("_jobProgress update:${_jobProgress.toString()}");
notifyListeners(); notifyListeners();
} else {
debugPrint(
"Failed to updateJobProgress ,id != _jobId,id:$id,_jobId:$_jobId");
}
} catch (e) { } catch (e) {
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
} }
@ -84,8 +80,7 @@ class FileModel extends ChangeNotifier {
jobDone(Map<String, dynamic> evt) { jobDone(Map<String, dynamic> evt) {
_jobProgress.state = JobState.done; _jobProgress.state = JobState.done;
// TODO refresh();
notifyListeners(); notifyListeners();
} }
@ -96,6 +91,11 @@ class FileModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
jobReset() {
_jobProgress.clear();
notifyListeners();
}
tryUpdateDir(String fd, bool isLocal) { tryUpdateDir(String fd, bool isLocal) {
try { try {
final fileDir = FileDirectory.fromJson(jsonDecode(fd), _sortStyle); final fileDir = FileDirectory.fromJson(jsonDecode(fd), _sortStyle);
@ -128,16 +128,50 @@ class FileModel extends ChangeNotifier {
openDirectory(fd.parent); openDirectory(fd.parent);
} }
sendFiles(String path, String to, bool showHidden, bool isRemote) { sendFiles(SelectedItems items) {
_jobId++; if (items.isLocal == null) {
final msg = { debugPrint("Failed to sendFiles ,wrong path state");
"id": _jobId.toString(), return;
"path": path, }
"to": to, _jobProgress.state = JobState.inProgress;
"show_hidden": showHidden.toString(), final toPath =
"is_remote": isRemote.toString() // isRemote path的位置而不是to的位置 items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
}; items.items.forEach((from) {
FFI.setByName("send_files", jsonEncode(msg)); _jobId++;
final msg = {
"id": _jobId.toString(),
"path": from.path,
"to": Path.join(toPath, from.name),
"show_hidden": "false", // TODO showHidden
"is_remote": (!(items.isLocal!)).toString() // from的位置而不是to的位置
};
FFI.setByName("send_files", jsonEncode(msg));
});
}
removeAction(SelectedItems items) {
if (items.isLocal == null) {
debugPrint("Failed to removeFile ,wrong path state");
return;
}
items.items.forEach((entry) {
_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);
}
});
}
createDir(String path){
} }
changeSortStyle(SortBy sort) { changeSortStyle(SortBy sort) {
@ -168,7 +202,7 @@ class FileDirectory {
if (json['entries'] != null) { if (json['entries'] != null) {
entries = <Entry>[]; entries = <Entry>[];
json['entries'].forEach((v) { json['entries'].forEach((v) {
entries.add(new Entry.fromJson(v)); entries.add(new Entry.fromJsonWithPath(v, path));
}); });
entries = _sortList(entries, sort); entries = _sortList(entries, sort);
} }
@ -189,15 +223,17 @@ class Entry {
int entryType = 4; int entryType = 4;
int modifiedTime = 0; int modifiedTime = 0;
String name = ""; String name = "";
String path = "";
int size = 0; int size = 0;
Entry(); Entry();
Entry.fromJson(Map<String, dynamic> json) { Entry.fromJsonWithPath(Map<String, dynamic> json, String parent) {
entryType = json['entry_type']; entryType = json['entry_type'];
modifiedTime = json['modified_time']; modifiedTime = json['modified_time'];
name = json['name']; name = json['name'];
size = json['size']; size = json['size'];
path = Path.join(parent, name);
} }
bool get isFile => entryType > 3; bool get isFile => entryType > 3;
@ -215,7 +251,7 @@ class JobProgress {
JobState state = JobState.none; JobState state = JobState.none;
var id = 0; var id = 0;
var fileNum = 0; var fileNum = 0;
var speed = 0; var speed = 0.0;
var finishedSize = 0; var finishedSize = 0;
clear() { clear() {

View File

@ -10,7 +10,6 @@ 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;
@ -38,7 +37,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
model.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
@ -51,53 +50,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
} }
@override @override
Widget build(BuildContext context) => Consumer<FileModel>(builder: (_context, _model, _child) { Widget build(BuildContext context) =>
return WillPopScope( Consumer<FileModel>(builder: (_context, _model, _child) {
onWillPop: () async { return WillPopScope(
if (model.selectMode) { onWillPop: () async {
model.toggleSelectMode(); if (model.selectMode) {
} else { model.toggleSelectMode();
goBack(); } else {
} goBack();
return false; }
}, return false;
child: Scaffold( },
backgroundColor: MyTheme.grayBg, child: Scaffold(
appBar: AppBar( backgroundColor: MyTheme.grayBg,
leading: Row(children: [ appBar: AppBar(
IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack), leading: Row(children: [
IconButton(icon: Icon(Icons.close), onPressed: clientClose), IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack),
]), IconButton(icon: Icon(Icons.close), onPressed: clientClose),
leadingWidth: 200, ]),
centerTitle: true, leadingWidth: 200,
title: Text(translate(model.isLocal ? "Local" : "Remote")), centerTitle: true,
actions: [ title: Text(translate(model.isLocal ? "Local" : "Remote")),
IconButton( actions: [
icon: Icon(Icons.change_circle), IconButton(
onPressed: ()=> model.togglePage(), icon: Icon(Icons.change_circle),
) onPressed: () => model.togglePage(),
], )
), ],
body: body(), ),
bottomSheet: bottomSheet(), body: body(),
)); bottomSheet: bottomSheet(),
}); ));
});
bool needShowCheckBox(){ bool needShowCheckBox() {
if(!model.selectMode){ if (!model.selectMode) {
return false; return false;
} }
return !_selectedItems.isOtherPage(model.isLocal); return !_selectedItems.isOtherPage(model.isLocal);
} }
Widget body() { Widget body() {
final isLocal = model.isLocal; final isLocal = model.isLocal;
final fd = model.currentDir; final fd = model.currentDir;
final entries = fd.entries; final entries = fd.entries;
return Column(children: [ return Column(children: [
headTools(), headTools(),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: entries.length + 1, itemCount: entries.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= entries.length) { if (index >= entries.length) {
@ -106,44 +106,61 @@ class _FileManagerPageState extends State<FileManagerPage> {
// 使 bottomSheet // 使 bottomSheet
return listTail(); return listTail();
} }
final path = Path.join(fd.path, entries[index].name);
var selected = false; var selected = false;
if (model.selectMode) { if (model.selectMode) {
selected = _selectedItems.contains(path); 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( return Card(
child: ListTile( child: ListTile(
leading: Icon(entries[index].isFile?Icons.feed_outlined:Icons.folder, leading: Icon(
size: 40), entries[index].isFile ? Icons.feed_outlined : Icons
.folder,
size: 40),
title: Text(entries[index].name), title: Text(entries[index].name),
selected: selected, selected: selected,
// subtitle: Text(entries[index].lastModified().toString()), subtitle: Text(
entries[index].lastModified().toString().replaceAll(
".000", "") + " " + sizeStr,style: TextStyle(fontSize: 12,color: MyTheme.darkGray),),
trailing: needShowCheckBox() 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) {
_selectedItems.add(isLocal,path); _selectedItems.add(isLocal, entries[index]);
} else if (!v && selected) { } else if (!v && selected) {
_selectedItems.remove(path); _selectedItems.remove(entries[index]);
} }
setState(() {}); setState(() {});
}) })
: null, : null,
onTap: () { onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { if (model.selectMode &&
!_selectedItems.isOtherPage(isLocal)) {
if (selected) { if (selected) {
_selectedItems.remove(path); _selectedItems.remove(entries[index]);
} else { } else {
_selectedItems.add(isLocal,path); _selectedItems.add(isLocal, entries[index]);
} }
setState(() {}); setState(() {});
return; return;
} }
if (entries[index].isDirectory) { if (entries[index].isDirectory) {
model.openDirectory(path); model.openDirectory(entries[index].path);
breadCrumbScrollToEnd(); breadCrumbScrollToEnd();
} else { } else {
// Perform file-related tasks. // Perform file-related tasks.
@ -153,7 +170,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
_selectedItems.clear(); _selectedItems.clear();
model.toggleSelectMode(); model.toggleSelectMode();
if (model.selectMode) { if (model.selectMode) {
_selectedItems.add(isLocal,path); _selectedItems.add(isLocal, entries[index]);
} }
setState(() {}); setState(() {});
}, },
@ -161,8 +178,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
); );
}, },
)) ))
]); ]);
} }
goBack() { goBack() {
model.goToParentDirectory(); model.goToParentDirectory();
@ -206,61 +223,68 @@ class _FileManagerPageState extends State<FileManagerPage> {
}); });
} }
Widget headTools() => Container( Widget headTools() =>
Container(
child: Row( 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: [ children: [
// IconButton(onPressed: () {}, icon: Icon(Icons.sort)), Expanded(
PopupMenuButton<SortBy>( child: BreadCrumb(
icon: Icon(Icons.sort), items: getPathBreadCrumbItems(() =>
itemBuilder: (context) { debugPrint("pressed home"),
return SortBy.values (e) => debugPrint("pressed url:$e")),
.map((e) => PopupMenuItem( divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow(
controller: _breadCrumbScroller),
)),
Row(
children: [
// IconButton(onPressed: () {}, icon: Icon(Icons.sort)),
PopupMenuButton<SortBy>(
icon: Icon(Icons.sort),
itemBuilder: (context) {
return SortBy.values
.map((e) =>
PopupMenuItem(
child: child:
Text(translate(e.toString().split(".").last)), Text(translate(e
.toString()
.split(".")
.last)),
value: e, value: e,
)) ))
.toList(); .toList();
}, },
onSelected: model.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: [Icon(Icons.refresh), Text("刷新")], children: [Icon(Icons.refresh), Text("刷新")],
), ),
value: "refresh", value: "refresh",
), ),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [Icon(Icons.check), Text("多选")], children: [Icon(Icons.check), Text("多选")],
), ),
value: "select", value: "select",
) )
]; ];
}, },
onSelected: (v) { onSelected: (v) {
if (v == "refresh") { if (v == "refresh") {
model.refresh(); model.refresh();
} else if (v == "select") { } else if (v == "select") {
_selectedItems.clear(); _selectedItems.clear();
model.toggleSelectMode(); model.toggleSelectMode();
} }
}), }),
],
)
], ],
) ));
],
));
Widget emptyPage() { Widget emptyPage() {
return Column( return Column(
@ -275,22 +299,123 @@ class _FileManagerPageState extends State<FileManagerPage> {
return SizedBox(height: 100); return SizedBox(height: 100);
} }
/// Widget? bottomSheet() {
/// localPage final state = model.jobState;
/// otherPage final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
/// final selectedItemsLength = "${_selectedItems.length} 个项目";
/// final local = _selectedItems.isLocal == null
BottomSheet? bottomSheet() { ? ""
if (!model.selectMode) return null; : " [${_selectedItems.isLocal! ? '本地' : '远程'}]";
if (model.selectMode) {
if (_selectedItems.length == 0 || !isOtherPage) {
//
return BottomSheetBody(
leading: Icon(Icons.check),
title: "已选择",
text: selectedItemsLength + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
if(_selectedItems.length>0){
model.removeAction(_selectedItems);
}
},
)
]);
} else {
//
return BottomSheetBody(
leading: Icon(Icons.input),
title: "粘贴到这里?",
text: selectedItemsLength + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.paste),
onPressed: () {
model.toggleSelectMode();
// TODO
model.sendFiles(_selectedItems);
},
)
]);
}
}
switch (state) {
case JobState.inProgress:
return BottomSheetBody(
leading: CircularProgressIndicator(),
title: "正在发送文件...",
text: "速度: ${(model.jobProgress.speed / 1024).toStringAsFixed(
2)} kb/s",
onCanceled: null,
);
case JobState.done:
return BottomSheetBody(
leading: Icon(Icons.check),
title: "发送成功!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.error:
return BottomSheetBody(
leading: Icon(Icons.error),
title: "发送错误!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.none:
break;
}
return null;
}
List<BreadCrumbItem> 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,
))
];
breadCrumbList.addAll(list.map((e) =>
BreadCrumbItem(
content: TextButton(
child: Text(e),
style:
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
onPressed: () => onPressed(e)))));
return breadCrumbList;
}
}
class BottomSheetBody extends StatelessWidget {
BottomSheetBody({required this.leading,
required this.title,
required this.text,
this.onCanceled,
this.actions});
final Widget leading;
final String title;
final String text;
final VoidCallback? onCanceled;
final List<IconButton>? actions;
@override
BottomSheet build(BuildContext context) {
final _actions = actions ?? [];
return BottomSheet( return BottomSheet(
backgroundColor: MyTheme.grayBg, builder: (BuildContext context) {
enableDrag: false, return Container(
onClosing: () {
debugPrint("BottomSheet close");
},
builder: (context) {
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
return Container(
height: 65, height: 65,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -301,112 +426,68 @@ class _FileManagerPageState extends State<FileManagerPage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// bottomSheet类框架
Row( Row(
children: [ children: [
CircularProgressIndicator(), leading,
isOtherPage?Icon(Icons.input):Icon(Icons.check),
SizedBox(width: 16), SizedBox(width: 16),
Column( Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(isOtherPage?'粘贴到这里?':'已选择',style: TextStyle(fontSize: 18)), Text(title, style: TextStyle(fontSize: 18)),
Text("${_selectedItems.length} 个文件 [${model.isLocal?'本地':'远程'}]",style: TextStyle(fontSize: 14,color: MyTheme.grayBg)) Text(text,
style: TextStyle(
fontSize: 14, color: MyTheme.grayBg))
], ],
) )
], ],
), ),
Row( Row(children: () {
children: [ _actions.add(IconButton(
(_selectedItems.length>0 && isOtherPage)? IconButton( icon: Icon(Icons.cancel_outlined),
icon: Icon(Icons.paste), onPressed: onCanceled,
onPressed:() { ));
debugPrint("paste"); return _actions;
// TODO  }())
model.sendFiles(
_selectedItems.items.first,
model.currentRemoteDir.path +
'/' +
_selectedItems.items.first.split('/').last,
false,
false);
// unused set callback
// _fileModel.set
},
):IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: () {
model.toggleSelectMode();
},
),
],
)
], ],
), ),
), ));
); },
}); onClosing: () {},
} backgroundColor: MyTheme.grayBg,
enableDrag: false,
List<BreadCrumbItem> getPathBreadCrumbItems( );
void Function() onHome, void Function(String) onPressed) {
final path = model.currentDir.path;
final list = path.trim().split('/'); // TODO use Path
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;
} }
} }
class SelectedItems { class SelectedItems {
bool? _isLocal; bool? _isLocal;
final List<String> _items = []; final List<Entry> _items = [];
List<String> get items => _items; List<Entry> get items => _items;
int get length => _items.length; int get length => _items.length;
// bool get isEmpty => _items.length == 0; bool? get isLocal => _isLocal;
add(bool isLocal, String path) { add(bool isLocal, Entry e) {
if (_isLocal == null) { if (_isLocal == null) {
_isLocal = isLocal; _isLocal = isLocal;
} }
if (_isLocal != null && _isLocal != isLocal) { if (_isLocal != null && _isLocal != isLocal) {
return; return;
} }
if (!_items.contains(path)) { if (!_items.contains(e)) {
_items.add(path); _items.add(e);
} }
} }
bool contains(String path) { bool contains(Entry e) {
return _items.contains(path); return _items.contains(e);
} }
remove(String path) { remove(Entry e) {
_items.remove(path); _items.remove(e);
if (_items.length == 0) { if (_items.length == 0) {
_isLocal = null; _isLocal = null;
} }