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

View File

@ -10,7 +10,6 @@ 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;
@ -38,7 +37,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
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
@ -51,53 +50,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
}
@override
Widget build(BuildContext context) => Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
goBack();
}
return false;
},
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
leading: Row(children: [
IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack),
IconButton(icon: Icon(Icons.close), onPressed: clientClose),
]),
leadingWidth: 200,
centerTitle: true,
title: Text(translate(model.isLocal ? "Local" : "Remote")),
actions: [
IconButton(
icon: Icon(Icons.change_circle),
onPressed: ()=> model.togglePage(),
)
],
),
body: body(),
bottomSheet: bottomSheet(),
));
});
Widget build(BuildContext context) =>
Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
goBack();
}
return false;
},
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
leading: Row(children: [
IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack),
IconButton(icon: Icon(Icons.close), onPressed: clientClose),
]),
leadingWidth: 200,
centerTitle: true,
title: Text(translate(model.isLocal ? "Local" : "Remote")),
actions: [
IconButton(
icon: Icon(Icons.change_circle),
onPressed: () => model.togglePage(),
)
],
),
body: body(),
bottomSheet: bottomSheet(),
));
});
bool needShowCheckBox(){
if(!model.selectMode){
bool needShowCheckBox() {
if (!model.selectMode) {
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
}
Widget body() {
final isLocal = model.isLocal;
final fd = model.currentDir;
final entries = fd.entries;
return Column(children: [
headTools(),
Expanded(
child: ListView.builder(
final isLocal = model.isLocal;
final fd = model.currentDir;
final entries = fd.entries;
return Column(children: [
headTools(),
Expanded(
child: ListView.builder(
itemCount: entries.length + 1,
itemBuilder: (context, index) {
if (index >= entries.length) {
@ -106,44 +106,61 @@ class _FileManagerPageState extends State<FileManagerPage> {
// 使 bottomSheet
return listTail();
}
final path = Path.join(fd.path, entries[index].name);
var selected = false;
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(
child: ListTile(
leading: Icon(entries[index].isFile?Icons.feed_outlined:Icons.folder,
size: 40),
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()),
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;
if (v && !selected) {
_selectedItems.add(isLocal,path);
} else if (!v && selected) {
_selectedItems.remove(path);
}
setState(() {});
})
value: selected,
onChanged: (v) {
if (v == null) return;
if (v && !selected) {
_selectedItems.add(isLocal, entries[index]);
} else if (!v && selected) {
_selectedItems.remove(entries[index]);
}
setState(() {});
})
: null,
onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
if (model.selectMode &&
!_selectedItems.isOtherPage(isLocal)) {
if (selected) {
_selectedItems.remove(path);
_selectedItems.remove(entries[index]);
} else {
_selectedItems.add(isLocal,path);
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
return;
}
if (entries[index].isDirectory) {
model.openDirectory(path);
model.openDirectory(entries[index].path);
breadCrumbScrollToEnd();
} else {
// Perform file-related tasks.
@ -153,7 +170,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
_selectedItems.clear();
model.toggleSelectMode();
if (model.selectMode) {
_selectedItems.add(isLocal,path);
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
},
@ -161,8 +178,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
);
},
))
]);
}
]);
}
goBack() {
model.goToParentDirectory();
@ -206,61 +223,68 @@ class _FileManagerPageState extends State<FileManagerPage> {
});
}
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: [
// IconButton(onPressed: () {}, icon: Icon(Icons.sort)),
PopupMenuButton<SortBy>(
icon: Icon(Icons.sort),
itemBuilder: (context) {
return SortBy.values
.map((e) => PopupMenuItem(
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<SortBy>(
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<String>(
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<String>(
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();
}
}),
],
)
],
)
],
));
));
Widget emptyPage() {
return Column(
@ -275,22 +299,123 @@ class _FileManagerPageState extends State<FileManagerPage> {
return SizedBox(height: 100);
}
///
/// localPage
/// otherPage
///
///
BottomSheet? bottomSheet() {
if (!model.selectMode) return null;
Widget? bottomSheet() {
final state = model.jobState;
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
final selectedItemsLength = "${_selectedItems.length} 个项目";
final local = _selectedItems.isLocal == 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(
backgroundColor: MyTheme.grayBg,
enableDrag: false,
onClosing: () {
debugPrint("BottomSheet close");
},
builder: (context) {
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
return Container(
builder: (BuildContext context) {
return Container(
height: 65,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
@ -301,112 +426,68 @@ class _FileManagerPageState extends State<FileManagerPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// bottomSheet类框架
Row(
children: [
CircularProgressIndicator(),
isOtherPage?Icon(Icons.input):Icon(Icons.check),
leading,
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))
Text(title, style: TextStyle(fontSize: 18)),
Text(text,
style: TextStyle(
fontSize: 14, color: MyTheme.grayBg))
],
)
],
),
Row(
children: [
(_selectedItems.length>0 && isOtherPage)? IconButton(
icon: Icon(Icons.paste),
onPressed:() {
debugPrint("paste");
// 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();
},
),
],
)
Row(children: () {
_actions.add(IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: onCanceled,
));
return _actions;
}())
],
),
),
);
});
}
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;
));
},
onClosing: () {},
backgroundColor: MyTheme.grayBg,
enableDrag: false,
);
}
}
class SelectedItems {
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;
// bool get isEmpty => _items.length == 0;
bool? get isLocal => _isLocal;
add(bool isLocal, String path) {
add(bool isLocal, Entry e) {
if (_isLocal == null) {
_isLocal = isLocal;
}
if (_isLocal != null && _isLocal != isLocal) {
return;
}
if (!_items.contains(path)) {
_items.add(path);
if (!_items.contains(e)) {
_items.add(e);
}
}
bool contains(String path) {
return _items.contains(path);
bool contains(Entry e) {
return _items.contains(e);
}
remove(String path) {
_items.remove(path);
remove(Entry e) {
_items.remove(e);
if (_items.length == 0) {
_isLocal = null;
}