file_manager_page.dart 27 KB

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