123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747 |
- import 'dart:async';
- import 'package:flutter/material.dart';
- import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
- import 'package:flutter_hbb/models/file_model.dart';
- import 'package:get/get.dart';
- import 'package:toggle_switch/toggle_switch.dart';
- import 'package:wakelock_plus/wakelock_plus.dart';
- import '../../common.dart';
- import '../../common/widgets/dialog.dart';
- class FileManagerPage extends StatefulWidget {
- FileManagerPage(
- {Key? key, required this.id, this.password, this.isSharedPassword})
- : super(key: key);
- final String id;
- final String? password;
- final bool? isSharedPassword;
- @override
- State<StatefulWidget> createState() => _FileManagerPageState();
- }
- enum SelectMode { local, remote, none }
- extension SelectModeEq on SelectMode {
- bool eq(bool? currentIsLocal) {
- if (currentIsLocal == null) {
- return false;
- }
- if (currentIsLocal) {
- return this == SelectMode.local;
- } else {
- return this == SelectMode.remote;
- }
- }
- }
- extension SelectModeExt on Rx<SelectMode> {
- void toggle(bool currentIsLocal) {
- switch (value) {
- case SelectMode.local:
- value = SelectMode.none;
- break;
- case SelectMode.remote:
- value = SelectMode.none;
- break;
- case SelectMode.none:
- if (currentIsLocal) {
- value = SelectMode.local;
- } else {
- value = SelectMode.remote;
- }
- break;
- }
- }
- }
- class _FileManagerPageState extends State<FileManagerPage> {
- final model = gFFI.fileModel;
- final selectMode = SelectMode.none.obs;
- var showLocal = true;
- FileController get currentFileController =>
- showLocal ? model.localController : model.remoteController;
- FileDirectory get currentDir => currentFileController.directory.value;
- DirectoryOptions get currentOptions => currentFileController.options.value;
- @override
- void initState() {
- super.initState();
- gFFI.start(widget.id,
- isFileTransfer: true,
- password: widget.password,
- isSharedPassword: widget.isSharedPassword);
- WidgetsBinding.instance.addPostFrameCallback((_) {
- gFFI.dialogManager
- .showLoading(translate('Connecting...'), onCancel: closeConnection);
- });
- gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id);
- WakelockPlus.enable();
- }
- @override
- void dispose() {
- model.close().whenComplete(() {
- gFFI.close();
- gFFI.dialogManager.dismissAll();
- WakelockPlus.disable();
- });
- super.dispose();
- }
- @override
- Widget build(BuildContext context) => WillPopScope(
- onWillPop: () async {
- if (selectMode.value != SelectMode.none) {
- selectMode.value = SelectMode.none;
- setState(() {});
- } else {
- currentFileController.goBack();
- }
- return false;
- },
- child: Scaffold(
- // backgroundColor: MyTheme.grayBg,
- appBar: AppBar(
- leading: Row(children: [
- IconButton(
- icon: Icon(Icons.close),
- onPressed: () =>
- clientClose(gFFI.sessionId, gFFI.dialogManager)),
- ]),
- centerTitle: true,
- title: ToggleSwitch(
- initialLabelIndex: showLocal ? 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,
- 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 = showLocal ? 0 : 1;
- if (index != current) {
- setState(() => showLocal = !showLocal);
- }
- },
- ),
- actions: [
- PopupMenuButton<String>(
- tooltip: "",
- icon: Icon(Icons.more_vert),
- itemBuilder: (context) {
- return [
- PopupMenuItem(
- child: Row(
- children: [
- Icon(Icons.refresh,
- color: Theme.of(context).iconTheme.color),
- SizedBox(width: 5),
- Text(translate("Refresh File"))
- ],
- ),
- value: "refresh",
- ),
- PopupMenuItem(
- enabled: currentDir.path != "/",
- child: Row(
- children: [
- Icon(Icons.check,
- color: Theme.of(context).iconTheme.color),
- SizedBox(width: 5),
- Text(translate("Multi Select"))
- ],
- ),
- value: "select",
- ),
- PopupMenuItem(
- enabled: currentDir.path != "/",
- child: Row(
- children: [
- Icon(Icons.folder_outlined,
- color: Theme.of(context).iconTheme.color),
- SizedBox(width: 5),
- Text(translate("Create Folder"))
- ],
- ),
- value: "folder",
- ),
- PopupMenuItem(
- enabled: currentDir.path != "/",
- child: Row(
- children: [
- Icon(
- currentOptions.showHidden
- ? Icons.check_box_outlined
- : Icons.check_box_outline_blank,
- color: Theme.of(context).iconTheme.color),
- SizedBox(width: 5),
- Text(translate("Show Hidden Files"))
- ],
- ),
- value: "hidden",
- )
- ];
- },
- onSelected: (v) {
- if (v == "refresh") {
- currentFileController.refresh();
- } else if (v == "select") {
- model.localController.selectedItems.clear();
- model.remoteController.selectedItems.clear();
- selectMode.toggle(showLocal);
- setState(() {});
- } else if (v == "folder") {
- final name = TextEditingController();
- String? errorText;
- gFFI.dialogManager.show((setState, close, context) {
- name.addListener(() {
- if (errorText != null) {
- setState(() {
- errorText = null;
- });
- }
- });
- return CustomAlertDialog(
- title: Text(translate("Create Folder")),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextFormField(
- decoration: InputDecoration(
- labelText:
- translate("Please enter the folder name"),
- errorText: errorText,
- ),
- controller: name,
- ),
- ],
- ),
- actions: [
- dialogButton("Cancel",
- onPressed: () => close(false), isOutline: true),
- dialogButton("OK", onPressed: () {
- if (name.value.text.isNotEmpty) {
- if (!PathUtil.validName(
- name.value.text,
- currentFileController
- .options.value.isWindows)) {
- setState(() {
- errorText =
- translate("Invalid folder name");
- });
- return;
- }
- currentFileController.createDir(PathUtil.join(
- currentDir.path,
- name.value.text,
- currentOptions.isWindows));
- close();
- }
- })
- ]);
- });
- } else if (v == "hidden") {
- currentFileController.toggleShowHidden();
- }
- }),
- ],
- ),
- body: showLocal
- ? FileManagerView(
- controller: model.localController,
- selectMode: selectMode,
- )
- : FileManagerView(
- controller: model.remoteController,
- selectMode: selectMode,
- ),
- bottomSheet: bottomSheet(),
- ));
- Widget? bottomSheet() {
- return Obx(() {
- final selectedItems = getActiveSelectedItems();
- final jobTable = model.jobController.jobTable;
- final localLabel = selectedItems?.isLocal == null
- ? ""
- : " [${selectedItems!.isLocal ? translate("Local") : translate("Remote")}]";
- if (!(selectMode.value == SelectMode.none)) {
- final selectedItemsLen =
- "${selectedItems?.items.length ?? 0} ${translate("items")}";
- if (selectedItems == null ||
- selectedItems.items.isEmpty ||
- selectMode.value.eq(showLocal)) {
- return BottomSheetBody(
- leading: Icon(Icons.check),
- title: translate("Selected"),
- text: selectedItemsLen + localLabel,
- onCanceled: () {
- selectedItems?.items.clear();
- selectMode.value = SelectMode.none;
- setState(() {});
- },
- actions: [
- IconButton(
- icon: Icon(Icons.compare_arrows),
- onPressed: () => setState(() => showLocal = !showLocal),
- ),
- IconButton(
- icon: Icon(Icons.delete_forever),
- onPressed: selectedItems != null
- ? () async {
- if (selectedItems.items.isNotEmpty) {
- await currentFileController
- .removeAction(selectedItems);
- selectedItems.items.clear();
- selectMode.value = SelectMode.none;
- }
- }
- : null,
- )
- ]);
- } else {
- return BottomSheetBody(
- leading: Icon(Icons.input),
- title: translate("Paste here?"),
- text: selectedItemsLen + localLabel,
- onCanceled: () {
- selectedItems.items.clear();
- selectMode.value = SelectMode.none;
- setState(() {});
- },
- actions: [
- IconButton(
- icon: Icon(Icons.compare_arrows),
- onPressed: () => setState(() => showLocal = !showLocal),
- ),
- IconButton(
- icon: Icon(Icons.paste),
- onPressed: () {
- selectMode.value = SelectMode.none;
- final otherSide = showLocal
- ? model.remoteController
- : model.localController;
- final thisSideData =
- DirectoryData(currentDir, currentOptions);
- otherSide.sendFiles(selectedItems, thisSideData);
- selectedItems.items.clear();
- selectMode.value = SelectMode.none;
- },
- )
- ]);
- }
- }
- if (jobTable.isEmpty) {
- return Offstage();
- }
- switch (jobTable.last.state) {
- case JobState.inProgress:
- return BottomSheetBody(
- leading: CircularProgressIndicator(),
- title: translate("Waiting"),
- text:
- "${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s",
- onCanceled: () {
- model.jobController.cancelJob(jobTable.last.id);
- jobTable.clear();
- },
- );
- case JobState.done:
- return BottomSheetBody(
- leading: Icon(Icons.check),
- title: "${translate("Successful")}!",
- text: jobTable.last.display(),
- onCanceled: () => jobTable.clear(),
- );
- case JobState.error:
- return BottomSheetBody(
- leading: Icon(Icons.error),
- title: "${translate("Error")}!",
- text: "",
- onCanceled: () => jobTable.clear(),
- );
- case JobState.none:
- break;
- case JobState.paused:
- // TODO: Handle this case.
- break;
- }
- return Offstage();
- });
- }
- SelectedItems? getActiveSelectedItems() {
- final localSelectedItems = model.localController.selectedItems;
- final remoteSelectedItems = model.remoteController.selectedItems;
- if (localSelectedItems.items.isNotEmpty &&
- remoteSelectedItems.items.isNotEmpty) {
- // assert unreachable
- debugPrint("Wrong SelectedItems state, reset");
- localSelectedItems.clear();
- remoteSelectedItems.clear();
- }
- if (localSelectedItems.items.isEmpty && remoteSelectedItems.items.isEmpty) {
- return null;
- }
- if (localSelectedItems.items.length > remoteSelectedItems.items.length) {
- return localSelectedItems;
- } else {
- return remoteSelectedItems;
- }
- }
- }
- class FileManagerView extends StatefulWidget {
- final FileController controller;
- final Rx<SelectMode> selectMode;
- FileManagerView({required this.controller, required this.selectMode});
- @override
- State<StatefulWidget> createState() => _FileManagerViewState();
- }
- class _FileManagerViewState extends State<FileManagerView> {
- final _listScrollController = ScrollController();
- final _breadCrumbScroller = ScrollController();
- bool get isLocal => widget.controller.isLocal;
- FileController get controller => widget.controller;
- SelectedItems get _selectedItems => widget.controller.selectedItems;
- @override
- void initState() {
- super.initState();
- controller.directory.listen((e) => breadCrumbScrollToEnd());
- }
- @override
- Widget build(BuildContext context) {
- return Column(children: [
- headTools(),
- Expanded(child: Obx(() {
- final entries = controller.directory.value.entries;
- return ListView.builder(
- controller: _listScrollController,
- itemCount: entries.length + 1,
- itemBuilder: (context, index) {
- if (index >= entries.length) {
- return listTail();
- }
- var selected = false;
- if (widget.selectMode.value != SelectMode.none) {
- selected = _selectedItems.items.contains(entries[index]);
- }
- final sizeStr = entries[index].isFile
- ? readableFileSize(entries[index].size.toDouble())
- : "";
- final showCheckBox = () {
- return widget.selectMode.value != SelectMode.none &&
- widget.selectMode.value.eq(controller.selectedItems.isLocal);
- }();
- 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(entries[index]);
- } else if (!v && selected) {
- _selectedItems.remove(entries[index]);
- }
- setState(() {});
- })
- : PopupMenuButton<String>(
- tooltip: "",
- 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,
- ),
- if (!entries[index].isDrive &&
- versionCmp(gFFI.ffiModel.pi.version,
- "1.3.0") >=
- 0)
- PopupMenuItem(
- child: Text(translate("Rename")),
- value: "rename",
- )
- ];
- },
- onSelected: (v) {
- if (v == "delete") {
- final items = SelectedItems(isLocal: isLocal);
- items.add(entries[index]);
- controller.removeAction(items);
- } else if (v == "multi_select") {
- _selectedItems.clear();
- widget.selectMode.toggle(isLocal);
- setState(() {});
- } else if (v == "rename") {
- controller.renameAction(
- entries[index], isLocal);
- }
- }),
- onTap: () {
- if (showCheckBox) {
- if (selected) {
- _selectedItems.remove(entries[index]);
- } else {
- _selectedItems.add(entries[index]);
- }
- setState(() {});
- return;
- }
- if (entries[index].isDirectory || entries[index].isDrive) {
- controller.openDirectory(entries[index].path);
- } else {
- // Perform file-related tasks.
- }
- },
- onLongPress: entries[index].isDrive
- ? null
- : () {
- _selectedItems.clear();
- widget.selectMode.toggle(isLocal);
- if (widget.selectMode.value != SelectMode.none) {
- _selectedItems.add(entries[index]);
- }
- setState(() {});
- },
- ),
- );
- },
- );
- }))
- ]);
- }
- void breadCrumbScrollToEnd() {
- Future.delayed(Duration(milliseconds: 200), () {
- if (_breadCrumbScroller.hasClients) {
- _breadCrumbScroller.animateTo(
- _breadCrumbScroller.position.maxScrollExtent,
- duration: Duration(milliseconds: 200),
- curve: Curves.fastLinearToSlowEaseIn);
- }
- });
- }
- Widget headTools() => Container(
- child: Row(
- children: [
- Expanded(child: Obx(() {
- final home = controller.options.value.home;
- final isWindows = controller.options.value.isWindows;
- return BreadCrumb(
- items: getPathBreadCrumbItems(controller.shortPath, isWindows,
- () => controller.goToHomeDirectory(), (list) {
- var path = "";
- if (home.startsWith(list[0])) {
- // absolute path
- for (var item in list) {
- path = PathUtil.join(path, item, isWindows);
- }
- } else {
- path += home;
- for (var item in list) {
- path = PathUtil.join(path, item, isWindows);
- }
- }
- controller.openDirectory(path);
- }),
- divider: Icon(Icons.chevron_right),
- overflow: ScrollableOverflow(controller: _breadCrumbScroller),
- );
- })),
- Row(
- children: [
- IconButton(
- icon: Icon(Icons.arrow_back),
- onPressed: controller.goBack,
- ),
- IconButton(
- icon: Icon(Icons.arrow_upward),
- onPressed: controller.goToParentDirectory,
- ),
- PopupMenuButton<SortBy>(
- tooltip: "",
- icon: Icon(Icons.sort),
- itemBuilder: (context) {
- return SortBy.values
- .map((e) => PopupMenuItem(
- child: Text(translate(e.toString())),
- value: e,
- ))
- .toList();
- },
- onSelected: controller.changeSortStyle),
- ],
- )
- ],
- ));
- Widget listTail() => Obx(() => Container(
- height: 100,
- child: Column(
- children: [
- Padding(
- padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
- child: Text(
- controller.directory.value.path,
- style: TextStyle(color: MyTheme.darkGray),
- ),
- ),
- Padding(
- padding: EdgeInsets.all(2),
- child: Text(
- "${translate("Total")}: ${controller.directory.value.entries.length} ${translate("items")}",
- style: TextStyle(color: MyTheme.darkGray),
- ),
- )
- ],
- ),
- ));
- List<BreadCrumbItem> getPathBreadCrumbItems(String shortPath, bool isWindows,
- void Function() onHome, void Function(List<String>) onPressed) {
- final list = PathUtil.split(shortPath, isWindows);
- final breadCrumbList = [
- BreadCrumbItem(
- 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) {
- // ignore: no_leading_underscores_for_local_identifiers
- final _actions = actions ?? [];
- return BottomSheet(
- builder: (BuildContext context) {
- return Container(
- 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,
- SizedBox(width: 16),
- Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(title, style: TextStyle(fontSize: 18)),
- Text(text,
- style: TextStyle(fontSize: 14)) // TODO color
- ],
- )
- ],
- ),
- Row(children: () {
- _actions.add(IconButton(
- icon: Icon(Icons.cancel_outlined),
- onPressed: onCanceled,
- ));
- return _actions;
- }())
- ],
- ),
- ));
- },
- onClosing: () {},
- // backgroundColor: MyTheme.grayBg,
- enableDrag: false,
- );
- }
- }
|