rustdesk/lib/pages/file_manager_page.dart

509 lines
16 KiB
Dart
Raw Normal View History

2022-03-07 22:54:34 +08:00
import 'dart:async';
import 'package:flutter/material.dart';
2022-03-07 22:54:34 +08:00
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:provider/provider.dart';
2022-03-09 17:07:24 +08:00
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
2022-03-11 01:28:13 +08:00
import 'package:path/path.dart' as Path;
2022-03-07 22:54:34 +08:00
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;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
}
class _FileManagerPageState extends State<FileManagerPage> {
2022-03-11 01:28:13 +08:00
final model = FFI.fileModel;
final _selectedItems = SelectedItems();
2022-03-07 22:54:34 +08:00
Timer? _interval;
Timer? _timer;
var _reconnects = 1;
2022-03-11 01:28:13 +08:00
final _breadCrumbScroller = ScrollController();
2022-03-07 22:54:34 +08:00
@override
void initState() {
super.initState();
showLoading(translate('Connecting...'));
FFI.connect(widget.id, isFileTransfer: true);
2022-03-11 01:28:13 +08:00
final res = FFI.getByName("read_dir", FFI.getByName("get_home_dir"));
debugPrint("read_dir local :$res");
model.tryUpdateDir(res, true);
2022-03-07 22:54:34 +08:00
_interval = Timer.periodic(Duration(milliseconds: 30),
(timer) => FFI.ffiModel.update(widget.id, context, handleMsgBox));
2022-03-07 22:54:34 +08:00
}
@override
void dispose() {
2022-03-11 01:28:13 +08:00
model.clear();
2022-03-07 22:54:34 +08:00
_interval?.cancel();
FFI.close();
EasyLoading.dismiss();
super.dispose();
}
@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(),
));
});
bool needShowCheckBox() {
if (!model.selectMode) {
2022-03-11 01:28:13 +08:00
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
2022-03-07 22:54:34 +08:00
}
2022-03-11 01:28:13 +08:00
Widget body() {
final isLocal = model.isLocal;
final fd = model.currentDir;
final entries = fd.entries;
return Column(children: [
headTools(),
Expanded(
child: ListView.builder(
2022-03-09 22:43:05 +08:00
itemCount: entries.length + 1,
itemBuilder: (context, index) {
if (index >= entries.length) {
// 添加尾部信息 文件统计信息等
// 添加快速返回上部
// 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多
return listTail();
}
var selected = false;
2022-03-11 01:28:13 +08:00
if (model.selectMode) {
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";
}
2022-03-09 22:43:05 +08:00
}
return Card(
child: ListTile(
leading: Icon(
entries[index].isFile ? Icons.feed_outlined : Icons
.folder,
size: 40),
2022-03-11 01:28:13 +08:00
2022-03-09 22:43:05 +08:00
title: Text(entries[index].name),
2022-03-11 01:28:13 +08:00
selected: selected,
subtitle: Text(
entries[index].lastModified().toString().replaceAll(
".000", "") + " " + sizeStr,style: TextStyle(fontSize: 12,color: MyTheme.darkGray),),
2022-03-11 01:28:13 +08:00
trailing: needShowCheckBox()
2022-03-09 22:43:05 +08:00
? Checkbox(
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(() {});
})
2022-03-09 22:43:05 +08:00
: null,
onTap: () {
if (model.selectMode &&
!_selectedItems.isOtherPage(isLocal)) {
2022-03-11 01:28:13 +08:00
if (selected) {
_selectedItems.remove(entries[index]);
2022-03-11 01:28:13 +08:00
} else {
_selectedItems.add(isLocal, entries[index]);
2022-03-11 01:28:13 +08:00
}
setState(() {});
return;
}
2022-03-09 22:43:05 +08:00
if (entries[index].isDirectory) {
model.openDirectory(entries[index].path);
2022-03-11 01:28:13 +08:00
breadCrumbScrollToEnd();
2022-03-09 22:43:05 +08:00
} else {
// Perform file-related tasks.
}
},
onLongPress: () {
2022-03-11 01:28:13 +08:00
_selectedItems.clear();
model.toggleSelectMode();
if (model.selectMode) {
_selectedItems.add(isLocal, entries[index]);
2022-03-11 01:28:13 +08:00
}
setState(() {});
2022-03-09 22:43:05 +08:00
},
),
);
},
))
]);
}
2022-03-09 17:07:24 +08:00
2022-03-07 22:54:34 +08:00
goBack() {
2022-03-11 01:28:13 +08:00
model.goToParentDirectory();
2022-03-07 22:54:34 +08:00
}
void handleMsgBox(Map<String, dynamic> evt, String id) {
var type = evt['type'];
var title = evt['title'];
var text = evt['text'];
if (type == 're-input-password') {
wrongPasswordDialog(id);
} else if (type == 'input-password') {
enterPasswordDialog(id);
} else {
var hasRetry = evt['hasRetry'] == 'true';
print(evt);
showMsgBox(type, title, text, hasRetry);
}
}
void showMsgBox(String type, String title, String text, bool hasRetry) {
msgBox(type, title, text);
if (hasRetry) {
_timer?.cancel();
_timer = Timer(Duration(seconds: _reconnects), () {
FFI.reconnect();
showLoading(translate('Connecting...'));
});
_reconnects *= 2;
} else {
_reconnects = 1;
}
}
2022-03-09 17:07:24 +08:00
2022-03-11 01:28:13 +08:00
breadCrumbScrollToEnd() {
Future.delayed(Duration(milliseconds: 200), () {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
});
}
Widget headTools() =>
Container(
2022-03-09 17:07:24 +08:00
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(
2022-03-09 22:43:05 +08:00
child:
Text(translate(e
.toString()
.split(".")
.last)),
2022-03-09 22:43:05 +08:00
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();
}
}),
],
)
2022-03-09 17:07:24 +08:00
],
));
2022-03-09 17:07:24 +08:00
Widget emptyPage() {
return Column(
children: [
headTools(),
Expanded(child: Center(child: Text("Empty Directory")))
],
);
}
Widget listTail() {
return SizedBox(height: 100);
}
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 ?? [];
2022-03-09 17:07:24 +08:00
return BottomSheet(
builder: (BuildContext context) {
return Container(
2022-03-09 17:07:24 +08:00
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: [
leading,
2022-03-11 01:28:13 +08:00
SizedBox(width: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 18)),
Text(text,
style: TextStyle(
fontSize: 14, color: MyTheme.grayBg))
2022-03-11 01:28:13 +08:00
],
)
2022-03-09 17:07:24 +08:00
],
),
Row(children: () {
_actions.add(IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: onCanceled,
));
return _actions;
}())
2022-03-09 17:07:24 +08:00
],
),
));
},
onClosing: () {},
backgroundColor: MyTheme.grayBg,
enableDrag: false,
);
2022-03-09 17:07:24 +08:00
}
2022-03-11 01:28:13 +08:00
}
class SelectedItems {
bool? _isLocal;
final List<Entry> _items = [];
2022-03-11 01:28:13 +08:00
List<Entry> get items => _items;
2022-03-11 01:28:13 +08:00
int get length => _items.length;
bool? get isLocal => _isLocal;
2022-03-11 01:28:13 +08:00
add(bool isLocal, Entry e) {
2022-03-11 01:28:13 +08:00
if (_isLocal == null) {
_isLocal = isLocal;
}
if (_isLocal != null && _isLocal != isLocal) {
return;
}
if (!_items.contains(e)) {
_items.add(e);
2022-03-11 01:28:13 +08:00
}
}
2022-03-09 17:07:24 +08:00
bool contains(Entry e) {
return _items.contains(e);
2022-03-11 01:28:13 +08:00
}
remove(Entry e) {
_items.remove(e);
2022-03-11 01:28:13 +08:00
if (_items.length == 0) {
_isLocal = null;
}
}
bool isOtherPage(bool currentIsLocal) {
if (_isLocal == null) {
return false;
} else {
return _isLocal != currentIsLocal;
}
}
clear() {
_items.clear();
_isLocal = null;
}
}