file_manager_page.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
  4. import 'package:flutter_hbb/models/file_model.dart';
  5. import 'package:get/get.dart';
  6. import 'package:toggle_switch/toggle_switch.dart';
  7. import 'package:wakelock_plus/wakelock_plus.dart';
  8. import '../../common.dart';
  9. import '../../common/widgets/dialog.dart';
  10. class FileManagerPage extends StatefulWidget {
  11. FileManagerPage(
  12. {Key? key, required this.id, this.password, this.isSharedPassword})
  13. : super(key: key);
  14. final String id;
  15. final String? password;
  16. final bool? isSharedPassword;
  17. @override
  18. State<StatefulWidget> createState() => _FileManagerPageState();
  19. }
  20. enum SelectMode { local, remote, none }
  21. extension SelectModeEq on SelectMode {
  22. bool eq(bool? currentIsLocal) {
  23. if (currentIsLocal == null) {
  24. return false;
  25. }
  26. if (currentIsLocal) {
  27. return this == SelectMode.local;
  28. } else {
  29. return this == SelectMode.remote;
  30. }
  31. }
  32. }
  33. extension SelectModeExt on Rx<SelectMode> {
  34. void toggle(bool currentIsLocal) {
  35. switch (value) {
  36. case SelectMode.local:
  37. value = SelectMode.none;
  38. break;
  39. case SelectMode.remote:
  40. value = SelectMode.none;
  41. break;
  42. case SelectMode.none:
  43. if (currentIsLocal) {
  44. value = SelectMode.local;
  45. } else {
  46. value = SelectMode.remote;
  47. }
  48. break;
  49. }
  50. }
  51. }
  52. class _FileManagerPageState extends State<FileManagerPage> {
  53. final model = gFFI.fileModel;
  54. final selectMode = SelectMode.none.obs;
  55. var showLocal = true;
  56. FileController get currentFileController =>
  57. showLocal ? model.localController : model.remoteController;
  58. FileDirectory get currentDir => currentFileController.directory.value;
  59. DirectoryOptions get currentOptions => currentFileController.options.value;
  60. @override
  61. void initState() {
  62. super.initState();
  63. gFFI.start(widget.id,
  64. isFileTransfer: true,
  65. password: widget.password,
  66. isSharedPassword: widget.isSharedPassword);
  67. WidgetsBinding.instance.addPostFrameCallback((_) {
  68. gFFI.dialogManager
  69. .showLoading(translate('Connecting...'), onCancel: closeConnection);
  70. });
  71. gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id);
  72. WakelockPlus.enable();
  73. }
  74. @override
  75. void dispose() {
  76. model.close().whenComplete(() {
  77. gFFI.close();
  78. gFFI.dialogManager.dismissAll();
  79. WakelockPlus.disable();
  80. });
  81. super.dispose();
  82. }
  83. @override
  84. Widget build(BuildContext context) => WillPopScope(
  85. onWillPop: () async {
  86. if (selectMode.value != SelectMode.none) {
  87. selectMode.value = SelectMode.none;
  88. setState(() {});
  89. } else {
  90. currentFileController.goBack();
  91. }
  92. return false;
  93. },
  94. child: Scaffold(
  95. // backgroundColor: MyTheme.grayBg,
  96. appBar: AppBar(
  97. leading: Row(children: [
  98. IconButton(
  99. icon: Icon(Icons.close),
  100. onPressed: () =>
  101. clientClose(gFFI.sessionId, gFFI.dialogManager)),
  102. ]),
  103. centerTitle: true,
  104. title: ToggleSwitch(
  105. initialLabelIndex: showLocal ? 0 : 1,
  106. activeBgColor: [MyTheme.idColor],
  107. inactiveBgColor: Theme.of(context).brightness == Brightness.light
  108. ? MyTheme.grayBg
  109. : null,
  110. inactiveFgColor: Theme.of(context).brightness == Brightness.light
  111. ? Colors.black54
  112. : null,
  113. totalSwitches: 2,
  114. minWidth: 100,
  115. fontSize: 15,
  116. iconSize: 18,
  117. labels: [translate("Local"), translate("Remote")],
  118. icons: [Icons.phone_android_sharp, Icons.screen_share],
  119. onToggle: (index) {
  120. final current = showLocal ? 0 : 1;
  121. if (index != current) {
  122. setState(() => showLocal = !showLocal);
  123. }
  124. },
  125. ),
  126. actions: [
  127. PopupMenuButton<String>(
  128. tooltip: "",
  129. icon: Icon(Icons.more_vert),
  130. itemBuilder: (context) {
  131. return [
  132. PopupMenuItem(
  133. child: Row(
  134. children: [
  135. Icon(Icons.refresh,
  136. color: Theme.of(context).iconTheme.color),
  137. SizedBox(width: 5),
  138. Text(translate("Refresh File"))
  139. ],
  140. ),
  141. value: "refresh",
  142. ),
  143. PopupMenuItem(
  144. enabled: currentDir.path != "/",
  145. child: Row(
  146. children: [
  147. Icon(Icons.check,
  148. color: Theme.of(context).iconTheme.color),
  149. SizedBox(width: 5),
  150. Text(translate("Multi Select"))
  151. ],
  152. ),
  153. value: "select",
  154. ),
  155. PopupMenuItem(
  156. enabled: currentDir.path != "/",
  157. child: Row(
  158. children: [
  159. Icon(Icons.folder_outlined,
  160. color: Theme.of(context).iconTheme.color),
  161. SizedBox(width: 5),
  162. Text(translate("Create Folder"))
  163. ],
  164. ),
  165. value: "folder",
  166. ),
  167. PopupMenuItem(
  168. enabled: currentDir.path != "/",
  169. child: Row(
  170. children: [
  171. Icon(
  172. currentOptions.showHidden
  173. ? Icons.check_box_outlined
  174. : Icons.check_box_outline_blank,
  175. color: Theme.of(context).iconTheme.color),
  176. SizedBox(width: 5),
  177. Text(translate("Show Hidden Files"))
  178. ],
  179. ),
  180. value: "hidden",
  181. )
  182. ];
  183. },
  184. onSelected: (v) {
  185. if (v == "refresh") {
  186. currentFileController.refresh();
  187. } else if (v == "select") {
  188. model.localController.selectedItems.clear();
  189. model.remoteController.selectedItems.clear();
  190. selectMode.toggle(showLocal);
  191. setState(() {});
  192. } else if (v == "folder") {
  193. final name = TextEditingController();
  194. String? errorText;
  195. gFFI.dialogManager.show((setState, close, context) {
  196. name.addListener(() {
  197. if (errorText != null) {
  198. setState(() {
  199. errorText = null;
  200. });
  201. }
  202. });
  203. return CustomAlertDialog(
  204. title: Text(translate("Create Folder")),
  205. content: Column(
  206. mainAxisSize: MainAxisSize.min,
  207. children: [
  208. TextFormField(
  209. decoration: InputDecoration(
  210. labelText:
  211. translate("Please enter the folder name"),
  212. errorText: errorText,
  213. ),
  214. controller: name,
  215. ),
  216. ],
  217. ),
  218. actions: [
  219. dialogButton("Cancel",
  220. onPressed: () => close(false), isOutline: true),
  221. dialogButton("OK", onPressed: () {
  222. if (name.value.text.isNotEmpty) {
  223. if (!PathUtil.validName(
  224. name.value.text,
  225. currentFileController
  226. .options.value.isWindows)) {
  227. setState(() {
  228. errorText =
  229. translate("Invalid folder name");
  230. });
  231. return;
  232. }
  233. currentFileController.createDir(PathUtil.join(
  234. currentDir.path,
  235. name.value.text,
  236. currentOptions.isWindows));
  237. close();
  238. }
  239. })
  240. ]);
  241. });
  242. } else if (v == "hidden") {
  243. currentFileController.toggleShowHidden();
  244. }
  245. }),
  246. ],
  247. ),
  248. body: showLocal
  249. ? FileManagerView(
  250. controller: model.localController,
  251. selectMode: selectMode,
  252. )
  253. : FileManagerView(
  254. controller: model.remoteController,
  255. selectMode: selectMode,
  256. ),
  257. bottomSheet: bottomSheet(),
  258. ));
  259. Widget? bottomSheet() {
  260. return Obx(() {
  261. final selectedItems = getActiveSelectedItems();
  262. final jobTable = model.jobController.jobTable;
  263. final localLabel = selectedItems?.isLocal == null
  264. ? ""
  265. : " [${selectedItems!.isLocal ? translate("Local") : translate("Remote")}]";
  266. if (!(selectMode.value == SelectMode.none)) {
  267. final selectedItemsLen =
  268. "${selectedItems?.items.length ?? 0} ${translate("items")}";
  269. if (selectedItems == null ||
  270. selectedItems.items.isEmpty ||
  271. selectMode.value.eq(showLocal)) {
  272. return BottomSheetBody(
  273. leading: Icon(Icons.check),
  274. title: translate("Selected"),
  275. text: selectedItemsLen + localLabel,
  276. onCanceled: () {
  277. selectedItems?.items.clear();
  278. selectMode.value = SelectMode.none;
  279. setState(() {});
  280. },
  281. actions: [
  282. IconButton(
  283. icon: Icon(Icons.compare_arrows),
  284. onPressed: () => setState(() => showLocal = !showLocal),
  285. ),
  286. IconButton(
  287. icon: Icon(Icons.delete_forever),
  288. onPressed: selectedItems != null
  289. ? () async {
  290. if (selectedItems.items.isNotEmpty) {
  291. await currentFileController
  292. .removeAction(selectedItems);
  293. selectedItems.items.clear();
  294. selectMode.value = SelectMode.none;
  295. }
  296. }
  297. : null,
  298. )
  299. ]);
  300. } else {
  301. return BottomSheetBody(
  302. leading: Icon(Icons.input),
  303. title: translate("Paste here?"),
  304. text: selectedItemsLen + localLabel,
  305. onCanceled: () {
  306. selectedItems.items.clear();
  307. selectMode.value = SelectMode.none;
  308. setState(() {});
  309. },
  310. actions: [
  311. IconButton(
  312. icon: Icon(Icons.compare_arrows),
  313. onPressed: () => setState(() => showLocal = !showLocal),
  314. ),
  315. IconButton(
  316. icon: Icon(Icons.paste),
  317. onPressed: () {
  318. selectMode.value = SelectMode.none;
  319. final otherSide = showLocal
  320. ? model.remoteController
  321. : model.localController;
  322. final thisSideData =
  323. DirectoryData(currentDir, currentOptions);
  324. otherSide.sendFiles(selectedItems, thisSideData);
  325. selectedItems.items.clear();
  326. selectMode.value = SelectMode.none;
  327. },
  328. )
  329. ]);
  330. }
  331. }
  332. if (jobTable.isEmpty) {
  333. return Offstage();
  334. }
  335. switch (jobTable.last.state) {
  336. case JobState.inProgress:
  337. return BottomSheetBody(
  338. leading: CircularProgressIndicator(),
  339. title: translate("Waiting"),
  340. text:
  341. "${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s",
  342. onCanceled: () {
  343. model.jobController.cancelJob(jobTable.last.id);
  344. jobTable.clear();
  345. },
  346. );
  347. case JobState.done:
  348. return BottomSheetBody(
  349. leading: Icon(Icons.check),
  350. title: "${translate("Successful")}!",
  351. text: jobTable.last.display(),
  352. onCanceled: () => jobTable.clear(),
  353. );
  354. case JobState.error:
  355. return BottomSheetBody(
  356. leading: Icon(Icons.error),
  357. title: "${translate("Error")}!",
  358. text: "",
  359. onCanceled: () => jobTable.clear(),
  360. );
  361. case JobState.none:
  362. break;
  363. case JobState.paused:
  364. // TODO: Handle this case.
  365. break;
  366. }
  367. return Offstage();
  368. });
  369. }
  370. SelectedItems? getActiveSelectedItems() {
  371. final localSelectedItems = model.localController.selectedItems;
  372. final remoteSelectedItems = model.remoteController.selectedItems;
  373. if (localSelectedItems.items.isNotEmpty &&
  374. remoteSelectedItems.items.isNotEmpty) {
  375. // assert unreachable
  376. debugPrint("Wrong SelectedItems state, reset");
  377. localSelectedItems.clear();
  378. remoteSelectedItems.clear();
  379. }
  380. if (localSelectedItems.items.isEmpty && remoteSelectedItems.items.isEmpty) {
  381. return null;
  382. }
  383. if (localSelectedItems.items.length > remoteSelectedItems.items.length) {
  384. return localSelectedItems;
  385. } else {
  386. return remoteSelectedItems;
  387. }
  388. }
  389. }
  390. class FileManagerView extends StatefulWidget {
  391. final FileController controller;
  392. final Rx<SelectMode> selectMode;
  393. FileManagerView({required this.controller, required this.selectMode});
  394. @override
  395. State<StatefulWidget> createState() => _FileManagerViewState();
  396. }
  397. class _FileManagerViewState extends State<FileManagerView> {
  398. final _listScrollController = ScrollController();
  399. final _breadCrumbScroller = ScrollController();
  400. bool get isLocal => widget.controller.isLocal;
  401. FileController get controller => widget.controller;
  402. SelectedItems get _selectedItems => widget.controller.selectedItems;
  403. @override
  404. void initState() {
  405. super.initState();
  406. controller.directory.listen((e) => breadCrumbScrollToEnd());
  407. }
  408. @override
  409. Widget build(BuildContext context) {
  410. return Column(children: [
  411. headTools(),
  412. Expanded(child: Obx(() {
  413. final entries = controller.directory.value.entries;
  414. return ListView.builder(
  415. controller: _listScrollController,
  416. itemCount: entries.length + 1,
  417. itemBuilder: (context, index) {
  418. if (index >= entries.length) {
  419. return listTail();
  420. }
  421. var selected = false;
  422. if (widget.selectMode.value != SelectMode.none) {
  423. selected = _selectedItems.items.contains(entries[index]);
  424. }
  425. final sizeStr = entries[index].isFile
  426. ? readableFileSize(entries[index].size.toDouble())
  427. : "";
  428. final showCheckBox = () {
  429. return widget.selectMode.value != SelectMode.none &&
  430. widget.selectMode.value.eq(controller.selectedItems.isLocal);
  431. }();
  432. return Card(
  433. child: ListTile(
  434. leading: entries[index].isDrive
  435. ? Padding(
  436. padding: EdgeInsets.symmetric(vertical: 8),
  437. child: Image(
  438. image: iconHardDrive,
  439. fit: BoxFit.scaleDown,
  440. color: Theme.of(context)
  441. .iconTheme
  442. .color
  443. ?.withOpacity(0.7)))
  444. : Icon(
  445. entries[index].isFile
  446. ? Icons.feed_outlined
  447. : Icons.folder,
  448. size: 40),
  449. title: Text(entries[index].name),
  450. selected: selected,
  451. subtitle: entries[index].isDrive
  452. ? null
  453. : Text(
  454. "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr",
  455. style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
  456. ),
  457. trailing: entries[index].isDrive
  458. ? null
  459. : showCheckBox
  460. ? Checkbox(
  461. value: selected,
  462. onChanged: (v) {
  463. if (v == null) return;
  464. if (v && !selected) {
  465. _selectedItems.add(entries[index]);
  466. } else if (!v && selected) {
  467. _selectedItems.remove(entries[index]);
  468. }
  469. setState(() {});
  470. })
  471. : PopupMenuButton<String>(
  472. tooltip: "",
  473. icon: Icon(Icons.more_vert),
  474. itemBuilder: (context) {
  475. return [
  476. PopupMenuItem(
  477. child: Text(translate("Delete")),
  478. value: "delete",
  479. ),
  480. PopupMenuItem(
  481. child: Text(translate("Multi Select")),
  482. value: "multi_select",
  483. ),
  484. PopupMenuItem(
  485. child: Text(translate("Properties")),
  486. value: "properties",
  487. enabled: false,
  488. ),
  489. if (!entries[index].isDrive &&
  490. versionCmp(gFFI.ffiModel.pi.version,
  491. "1.3.0") >=
  492. 0)
  493. PopupMenuItem(
  494. child: Text(translate("Rename")),
  495. value: "rename",
  496. )
  497. ];
  498. },
  499. onSelected: (v) {
  500. if (v == "delete") {
  501. final items = SelectedItems(isLocal: isLocal);
  502. items.add(entries[index]);
  503. controller.removeAction(items);
  504. } else if (v == "multi_select") {
  505. _selectedItems.clear();
  506. widget.selectMode.toggle(isLocal);
  507. setState(() {});
  508. } else if (v == "rename") {
  509. controller.renameAction(
  510. entries[index], isLocal);
  511. }
  512. }),
  513. onTap: () {
  514. if (showCheckBox) {
  515. if (selected) {
  516. _selectedItems.remove(entries[index]);
  517. } else {
  518. _selectedItems.add(entries[index]);
  519. }
  520. setState(() {});
  521. return;
  522. }
  523. if (entries[index].isDirectory || entries[index].isDrive) {
  524. controller.openDirectory(entries[index].path);
  525. } else {
  526. // Perform file-related tasks.
  527. }
  528. },
  529. onLongPress: entries[index].isDrive
  530. ? null
  531. : () {
  532. _selectedItems.clear();
  533. widget.selectMode.toggle(isLocal);
  534. if (widget.selectMode.value != SelectMode.none) {
  535. _selectedItems.add(entries[index]);
  536. }
  537. setState(() {});
  538. },
  539. ),
  540. );
  541. },
  542. );
  543. }))
  544. ]);
  545. }
  546. void breadCrumbScrollToEnd() {
  547. Future.delayed(Duration(milliseconds: 200), () {
  548. if (_breadCrumbScroller.hasClients) {
  549. _breadCrumbScroller.animateTo(
  550. _breadCrumbScroller.position.maxScrollExtent,
  551. duration: Duration(milliseconds: 200),
  552. curve: Curves.fastLinearToSlowEaseIn);
  553. }
  554. });
  555. }
  556. Widget headTools() => Container(
  557. child: Row(
  558. children: [
  559. Expanded(child: Obx(() {
  560. final home = controller.options.value.home;
  561. final isWindows = controller.options.value.isWindows;
  562. return BreadCrumb(
  563. items: getPathBreadCrumbItems(controller.shortPath, isWindows,
  564. () => controller.goToHomeDirectory(), (list) {
  565. var path = "";
  566. if (home.startsWith(list[0])) {
  567. // absolute path
  568. for (var item in list) {
  569. path = PathUtil.join(path, item, isWindows);
  570. }
  571. } else {
  572. path += home;
  573. for (var item in list) {
  574. path = PathUtil.join(path, item, isWindows);
  575. }
  576. }
  577. controller.openDirectory(path);
  578. }),
  579. divider: Icon(Icons.chevron_right),
  580. overflow: ScrollableOverflow(controller: _breadCrumbScroller),
  581. );
  582. })),
  583. Row(
  584. children: [
  585. IconButton(
  586. icon: Icon(Icons.arrow_back),
  587. onPressed: controller.goBack,
  588. ),
  589. IconButton(
  590. icon: Icon(Icons.arrow_upward),
  591. onPressed: controller.goToParentDirectory,
  592. ),
  593. PopupMenuButton<SortBy>(
  594. tooltip: "",
  595. icon: Icon(Icons.sort),
  596. itemBuilder: (context) {
  597. return SortBy.values
  598. .map((e) => PopupMenuItem(
  599. child: Text(translate(e.toString())),
  600. value: e,
  601. ))
  602. .toList();
  603. },
  604. onSelected: controller.changeSortStyle),
  605. ],
  606. )
  607. ],
  608. ));
  609. Widget listTail() => Obx(() => Container(
  610. height: 100,
  611. child: Column(
  612. children: [
  613. Padding(
  614. padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
  615. child: Text(
  616. controller.directory.value.path,
  617. style: TextStyle(color: MyTheme.darkGray),
  618. ),
  619. ),
  620. Padding(
  621. padding: EdgeInsets.all(2),
  622. child: Text(
  623. "${translate("Total")}: ${controller.directory.value.entries.length} ${translate("items")}",
  624. style: TextStyle(color: MyTheme.darkGray),
  625. ),
  626. )
  627. ],
  628. ),
  629. ));
  630. List<BreadCrumbItem> getPathBreadCrumbItems(String shortPath, bool isWindows,
  631. void Function() onHome, void Function(List<String>) onPressed) {
  632. final list = PathUtil.split(shortPath, isWindows);
  633. final breadCrumbList = [
  634. BreadCrumbItem(
  635. content: IconButton(
  636. icon: Icon(Icons.home_filled),
  637. onPressed: onHome,
  638. ))
  639. ];
  640. breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
  641. content: TextButton(
  642. child: Text(e.value),
  643. style:
  644. ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
  645. onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
  646. return breadCrumbList;
  647. }
  648. }
  649. class BottomSheetBody extends StatelessWidget {
  650. BottomSheetBody(
  651. {required this.leading,
  652. required this.title,
  653. required this.text,
  654. this.onCanceled,
  655. this.actions});
  656. final Widget leading;
  657. final String title;
  658. final String text;
  659. final VoidCallback? onCanceled;
  660. final List<IconButton>? actions;
  661. @override
  662. BottomSheet build(BuildContext context) {
  663. // ignore: no_leading_underscores_for_local_identifiers
  664. final _actions = actions ?? [];
  665. return BottomSheet(
  666. builder: (BuildContext context) {
  667. return Container(
  668. height: 65,
  669. alignment: Alignment.centerLeft,
  670. decoration: BoxDecoration(
  671. color: MyTheme.accent50,
  672. borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
  673. child: Padding(
  674. padding: EdgeInsets.symmetric(horizontal: 15),
  675. child: Row(
  676. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  677. children: [
  678. Row(
  679. children: [
  680. leading,
  681. SizedBox(width: 16),
  682. Column(
  683. mainAxisAlignment: MainAxisAlignment.center,
  684. crossAxisAlignment: CrossAxisAlignment.start,
  685. children: [
  686. Text(title, style: TextStyle(fontSize: 18)),
  687. Text(text,
  688. style: TextStyle(fontSize: 14)) // TODO color
  689. ],
  690. )
  691. ],
  692. ),
  693. Row(children: () {
  694. _actions.add(IconButton(
  695. icon: Icon(Icons.cancel_outlined),
  696. onPressed: onCanceled,
  697. ));
  698. return _actions;
  699. }())
  700. ],
  701. ),
  702. ));
  703. },
  704. onClosing: () {},
  705. // backgroundColor: MyTheme.grayBg,
  706. enableDrag: false,
  707. );
  708. }
  709. }