rustdesk/flutter/lib/mobile/pages/file_manager_page.dart

584 lines
21 KiB
Dart
Raw Normal View History

2022-03-07 22:54:34 +08:00
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
2022-03-07 22:54:34 +08:00
import 'package:flutter_hbb/models/file_model.dart';
import 'package:provider/provider.dart';
2022-04-07 16:20:32 +08:00
import 'package:toggle_switch/toggle_switch.dart';
import 'package:wakelock/wakelock.dart';
2022-05-24 23:33:00 +08:00
import '../../common.dart';
2022-03-07 22:54:34 +08:00
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> {
final model = gFFI.fileModel;
2022-03-11 01:28:13 +08:00
final _selectedItems = SelectedItems();
final _breadCrumbScroller = ScrollController();
2022-03-07 22:54:34 +08:00
@override
void initState() {
super.initState();
2022-09-27 20:35:02 +08:00
gFFI.start(widget.id, isFileTransfer: true);
2022-08-12 18:42:02 +08:00
WidgetsBinding.instance.addPostFrameCallback((_) {
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
2022-08-12 18:42:02 +08:00
});
gFFI.ffiModel.updateEventListener(widget.id);
model.onDirChanged = (_) => breadCrumbScrollToEnd();
2022-04-19 14:07:46 +08:00
Wakelock.enable();
2022-03-07 22:54:34 +08:00
}
@override
void dispose() {
model.onClose().whenComplete(() {
gFFI.close();
gFFI.dialogManager.dismissAll();
Wakelock.disable();
});
2022-03-07 22:54:34 +08:00
super.dispose();
}
@override
2022-03-25 16:34:27 +08:00
Widget build(BuildContext context) => ChangeNotifierProvider.value(
value: model,
2022-03-25 16:34:27 +08:00
child: Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
model.goBack();
}
return false;
},
child: Scaffold(
2022-09-23 16:31:50 +08:00
// backgroundColor: MyTheme.grayBg,
appBar: AppBar(
leading: Row(children: [
2022-08-12 18:42:02 +08:00
IconButton(
icon: Icon(Icons.close),
onPressed: () =>
clientClose(widget.id, gFFI.dialogManager)),
]),
centerTitle: true,
2022-04-07 16:20:32 +08:00
title: ToggleSwitch(
initialLabelIndex: model.isLocal ? 0 : 1,
activeBgColor: [MyTheme.idColor],
inactiveBgColor:
Theme.of(context).brightness == Brightness.light
? MyTheme.grayBg
: null,
inactiveFgColor:
Theme.of(context).brightness == Brightness.light
? Colors.black54
: null,
2022-04-07 16:20:32 +08:00
totalSwitches: 2,
minWidth: 100,
fontSize: 15,
iconSize: 18,
labels: [translate("Local"), translate("Remote")],
icons: [Icons.phone_android_sharp, Icons.screen_share],
onToggle: (index) {
final current = model.isLocal ? 0 : 1;
if (index != current) {
model.togglePage();
}
},
),
2022-04-07 20:40:51 +08:00
actions: [
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.refresh,
color: Theme.of(context).iconTheme.color),
2022-04-07 20:40:51 +08:00
SizedBox(width: 5),
Text(translate("Refresh File"))
],
),
value: "refresh",
),
PopupMenuItem(
enabled: model.currentDir.path != "/",
2022-04-07 20:40:51 +08:00
child: Row(
children: [
Icon(Icons.check,
color: Theme.of(context).iconTheme.color),
2022-04-07 20:40:51 +08:00
SizedBox(width: 5),
Text(translate("Multi Select"))
],
),
value: "select",
),
PopupMenuItem(
enabled: model.currentDir.path != "/",
2022-04-07 20:40:51 +08:00
child: Row(
children: [
Icon(Icons.folder_outlined,
color: Theme.of(context).iconTheme.color),
2022-04-07 20:40:51 +08:00
SizedBox(width: 5),
Text(translate("Create Folder"))
],
),
value: "folder",
),
PopupMenuItem(
enabled: model.currentDir.path != "/",
2022-04-07 20:40:51 +08:00
child: Row(
children: [
Icon(
model.getCurrentShowHidden()
2022-04-07 20:40:51 +08:00
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: Theme.of(context).iconTheme.color),
2022-04-07 20:40:51 +08:00
SizedBox(width: 5),
Text(translate("Show Hidden Files"))
],
),
value: "hidden",
)
];
},
onSelected: (v) {
if (v == "refresh") {
model.refresh();
} else if (v == "select") {
_selectedItems.clear();
model.toggleSelectMode();
} else if (v == "folder") {
final name = TextEditingController();
2022-08-12 18:42:02 +08:00
gFFI.dialogManager
.show((setState, close) => CustomAlertDialog(
2022-04-07 20:40:51 +08:00
title: Text(translate("Create Folder")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: translate(
"Please enter the folder name"),
),
controller: name,
),
],
),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () => close(false),
child: Text(translate("Cancel"))),
ElevatedButton(
style: flatButtonStyle,
onPressed: () {
if (name.value.text.isNotEmpty) {
model.createDir(PathUtil.join(
model.currentDir.path,
name.value.text,
model
.getCurrentIsWindows()));
2022-04-07 20:40:51 +08:00
close();
}
},
child: Text(translate("OK")))
]));
} else if (v == "hidden") {
model.toggleShowHidden();
}
}),
],
),
body: body(),
bottomSheet: bottomSheet(),
));
2022-03-25 16:34:27 +08:00
}));
bool showCheckBox() {
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(
controller: ScrollController(),
itemCount: entries.length + 1,
itemBuilder: (context, index) {
if (index >= entries.length) {
return listTail();
}
var selected = false;
if (model.selectMode) {
selected = _selectedItems.contains(entries[index]);
}
final sizeStr = entries[index].isFile
? readableFileSize(entries[index].size.toDouble())
: "";
return Card(
child: ListTile(
leading: entries[index].isDrive
? Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Image(
image: iconHardDrive,
fit: BoxFit.scaleDown,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7)))
: Icon(
entries[index].isFile
? Icons.feed_outlined
: Icons.folder,
size: 40),
title: Text(entries[index].name),
selected: selected,
subtitle: entries[index].isDrive
? null
: Text(
"${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr",
style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
),
trailing: entries[index].isDrive
? null
: showCheckBox()
? 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(() {});
})
: PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text(translate("Delete")),
value: "delete",
),
PopupMenuItem(
child: Text(translate("Multi Select")),
value: "multi_select",
),
PopupMenuItem(
child: Text(translate("Properties")),
value: "properties",
enabled: false,
)
];
},
onSelected: (v) {
if (v == "delete") {
final items = SelectedItems();
items.add(isLocal, entries[index]);
model.removeAction(items);
} else if (v == "multi_select") {
_selectedItems.clear();
model.toggleSelectMode();
}
}),
onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
if (selected) {
_selectedItems.remove(entries[index]);
} else {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
return;
}
if (entries[index].isDirectory || entries[index].isDrive) {
model.openDirectory(entries[index].path);
} else {
// Perform file-related tasks.
}
},
onLongPress: entries[index].isDrive
? null
: () {
_selectedItems.clear();
model.toggleSelectMode();
if (model.selectMode) {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
},
),
);
},
))
]);
}
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(() => model.goHome(), (list) {
var path = "";
if (model.currentHome.startsWith(list[0])) {
// absolute path
for (var item in list) {
path = PathUtil.join(path, item, model.getCurrentIsWindows());
}
} else {
path += model.currentHome;
for (var item in list) {
path = PathUtil.join(path, item, model.getCurrentIsWindows());
}
}
model.openDirectory(path);
}),
divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow(controller: _breadCrumbScroller),
)),
Row(
2022-03-09 17:07:24 +08:00
children: [
IconButton(
icon: Icon(Icons.arrow_back),
onPressed: model.goBack,
),
2022-04-07 20:40:51 +08:00
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: model.goToParentDirectory,
2022-04-07 20:40:51 +08:00
),
PopupMenuButton<SortBy>(
icon: Icon(Icons.sort),
itemBuilder: (context) {
return SortBy.values
.map((e) => PopupMenuItem(
child: Text(translate(e.toString())),
2022-03-09 22:43:05 +08:00
value: e,
))
.toList();
},
onSelected: model.changeSortStyle),
2022-03-09 17:07:24 +08:00
],
)
],
));
2022-03-09 17:07:24 +08:00
Widget listTail() {
return Container(
height: 100,
child: Column(
children: [
2022-04-07 20:40:51 +08:00
Padding(
2022-04-07 22:58:47 +08:00
padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
2022-04-07 20:40:51 +08:00
child: Text(
model.currentDir.path,
style: TextStyle(color: MyTheme.darkGray),
),
),
Padding(
padding: EdgeInsets.all(2),
child: Text(
2022-04-20 23:11:04 +08:00
"${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}",
style: TextStyle(color: MyTheme.darkGray),
),
)
],
),
);
2022-03-09 17:07:24 +08:00
}
Widget? bottomSheet() {
final state = model.jobState;
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
2022-03-25 16:34:27 +08:00
final selectedItemsLen = "${_selectedItems.length} ${translate("items")}";
final local = _selectedItems.isLocal == null
? ""
2022-03-23 15:28:21 +08:00
: " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
if (model.selectMode) {
if (_selectedItems.length == 0 || !isOtherPage) {
return BottomSheetBody(
leading: Icon(Icons.check),
2022-03-23 15:28:21 +08:00
title: translate("Selected"),
2022-03-25 16:34:27 +08:00
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
2022-04-07 23:19:57 +08:00
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
if (_selectedItems.length > 0) {
model.removeAction(_selectedItems);
}
},
)
]);
} else {
return BottomSheetBody(
leading: Icon(Icons.input),
2022-03-23 15:28:21 +08:00
title: translate("Paste here?"),
2022-03-25 16:34:27 +08:00
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
2022-04-07 23:19:57 +08:00
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.paste),
onPressed: () {
model.toggleSelectMode();
model.sendFiles(_selectedItems);
},
)
]);
}
}
switch (state) {
case JobState.inProgress:
return BottomSheetBody(
leading: CircularProgressIndicator(),
2022-03-23 15:28:21 +08:00
title: translate("Waiting"),
2022-03-25 16:34:27 +08:00
text:
"${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s",
2022-04-07 22:58:47 +08:00
onCanceled: () => model.cancelJob(model.jobProgress.id),
);
case JobState.done:
return BottomSheetBody(
leading: Icon(Icons.check),
2022-03-23 15:28:21 +08:00
title: "${translate("Successful")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.error:
return BottomSheetBody(
leading: Icon(Icons.error),
2022-03-23 15:28:21 +08:00
title: "${translate("Error")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.none:
break;
2022-09-23 16:31:50 +08:00
case JobState.paused:
// TODO: Handle this case.
break;
}
return null;
}
List<BreadCrumbItem> getPathBreadCrumbItems(
void Function() onHome, void Function(List<String>) onPressed) {
final path = model.currentShortPath;
final list = PathUtil.split(path, model.getCurrentIsWindows());
final breadCrumbList = [
BreadCrumbItem(
2022-04-07 20:40:51 +08:00
content: IconButton(
icon: Icon(Icons.home_filled),
onPressed: onHome,
))
];
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
content: TextButton(
child: Text(e.value),
style:
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
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,
2022-09-23 16:31:50 +08:00
style: TextStyle(fontSize: 14)) // TODO color
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: () {},
2022-09-23 16:31:50 +08:00
// backgroundColor: MyTheme.grayBg,
enableDrag: false,
);
2022-03-09 17:07:24 +08:00
}
2022-03-11 01:28:13 +08:00
}