delete_file_util.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:flutter/material.dart';
  5. import 'package:logging/logging.dart';
  6. import "package:path/path.dart";
  7. import 'package:photo_manager/photo_manager.dart';
  8. import 'package:photos/core/constants.dart';
  9. import 'package:photos/core/event_bus.dart';
  10. import 'package:photos/db/files_db.dart';
  11. import 'package:photos/events/collection_updated_event.dart';
  12. import 'package:photos/events/files_updated_event.dart';
  13. import "package:photos/events/force_reload_trash_page_event.dart";
  14. import 'package:photos/events/local_photos_updated_event.dart';
  15. import "package:photos/generated/l10n.dart";
  16. import 'package:photos/models/file/file.dart';
  17. import "package:photos/models/files_split.dart";
  18. import 'package:photos/models/selected_files.dart';
  19. import 'package:photos/models/trash_item_request.dart';
  20. import 'package:photos/services/remote_sync_service.dart';
  21. import 'package:photos/services/sync_service.dart';
  22. import 'package:photos/services/trash_sync_service.dart';
  23. import 'package:photos/ui/common/linear_progress_dialog.dart';
  24. import 'package:photos/ui/components/action_sheet_widget.dart';
  25. import 'package:photos/ui/components/buttons/button_widget.dart';
  26. import 'package:photos/ui/components/models/button_type.dart';
  27. import "package:photos/utils/device_info.dart";
  28. import 'package:photos/utils/dialog_util.dart';
  29. import 'package:photos/utils/file_util.dart';
  30. import 'package:photos/utils/toast_util.dart';
  31. final _logger = Logger("DeleteFileUtil");
  32. Future<void> deleteFilesFromEverywhere(
  33. BuildContext context,
  34. List<EnteFile> files,
  35. ) async {
  36. _logger.info("Trying to deleteFilesFromEverywhere " + files.toString());
  37. final List<String> localAssetIDs = [];
  38. final List<String> localSharedMediaIDs = [];
  39. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  40. bool hasLocalOnlyFiles = false;
  41. for (final file in files) {
  42. if (file.localID != null) {
  43. if (!(await _localFileExist(file))) {
  44. _logger.warning("Already deleted " + file.toString());
  45. alreadyDeletedIDs.add(file.localID!);
  46. } else if (file.isSharedMediaToAppSandbox) {
  47. localSharedMediaIDs.add(file.localID!);
  48. } else {
  49. localAssetIDs.add(file.localID!);
  50. }
  51. }
  52. if (file.uploadedFileID == null) {
  53. hasLocalOnlyFiles = true;
  54. }
  55. }
  56. if (hasLocalOnlyFiles && Platform.isAndroid) {
  57. final shouldProceed = await shouldProceedWithDeletion(context);
  58. if (!shouldProceed) {
  59. return;
  60. }
  61. }
  62. Set<String> deletedIDs = <String>{};
  63. try {
  64. deletedIDs =
  65. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  66. } catch (e, s) {
  67. _logger.severe("Could not delete file", e, s);
  68. }
  69. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  70. final updatedCollectionIDs = <int>{};
  71. final List<TrashRequest> uploadedFilesToBeTrashed = [];
  72. final List<EnteFile> deletedFiles = [];
  73. for (final file in files) {
  74. if (file.localID != null) {
  75. // Remove only those files that have already been removed from disk
  76. if (deletedIDs.contains(file.localID) ||
  77. alreadyDeletedIDs.contains(file.localID)) {
  78. deletedFiles.add(file);
  79. if (file.uploadedFileID != null) {
  80. uploadedFilesToBeTrashed
  81. .add(TrashRequest(file.uploadedFileID!, file.collectionID!));
  82. updatedCollectionIDs.add(file.collectionID!);
  83. } else {
  84. await FilesDB.instance.deleteLocalFile(file);
  85. }
  86. }
  87. } else {
  88. updatedCollectionIDs.add(file.collectionID!);
  89. deletedFiles.add(file);
  90. uploadedFilesToBeTrashed
  91. .add(TrashRequest(file.uploadedFileID!, file.collectionID!));
  92. }
  93. }
  94. if (uploadedFilesToBeTrashed.isNotEmpty) {
  95. try {
  96. final fileIDs =
  97. uploadedFilesToBeTrashed.map((item) => item.fileID).toList();
  98. await TrashSyncService.instance
  99. .trashFilesOnServer(uploadedFilesToBeTrashed);
  100. await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs);
  101. } catch (e) {
  102. _logger.severe(e);
  103. showGenericErrorDialog(context: context);
  104. rethrow;
  105. }
  106. for (final collectionID in updatedCollectionIDs) {
  107. Bus.instance.fire(
  108. CollectionUpdatedEvent(
  109. collectionID,
  110. deletedFiles
  111. .where((file) => file.collectionID == collectionID)
  112. .toList(),
  113. "deleteFilesEverywhere",
  114. type: EventType.deletedFromEverywhere,
  115. ),
  116. );
  117. }
  118. }
  119. if (deletedFiles.isNotEmpty) {
  120. Bus.instance.fire(
  121. LocalPhotosUpdatedEvent(
  122. deletedFiles,
  123. type: EventType.deletedFromEverywhere,
  124. source: "deleteFilesEverywhere",
  125. ),
  126. );
  127. if (hasLocalOnlyFiles && Platform.isAndroid) {
  128. showShortToast(context, S.of(context).filesDeleted);
  129. } else {
  130. showShortToast(context, S.of(context).movedToTrash);
  131. }
  132. }
  133. if (uploadedFilesToBeTrashed.isNotEmpty) {
  134. RemoteSyncService.instance.sync(silently: true);
  135. }
  136. }
  137. Future<void> deleteFilesFromRemoteOnly(
  138. BuildContext context,
  139. List<EnteFile> files,
  140. ) async {
  141. files.removeWhere((element) => element.uploadedFileID == null);
  142. if (files.isEmpty) {
  143. showToast(context, S.of(context).selectedFilesAreNotOnEnte);
  144. return;
  145. }
  146. _logger.info(
  147. "Trying to deleteFilesFromRemoteOnly " +
  148. files.map((f) => f.uploadedFileID).toString(),
  149. );
  150. final updatedCollectionIDs = <int>{};
  151. final List<int> uploadedFileIDs = [];
  152. final List<TrashRequest> trashRequests = [];
  153. for (final file in files) {
  154. updatedCollectionIDs.add(file.collectionID!);
  155. uploadedFileIDs.add(file.uploadedFileID!);
  156. trashRequests.add(TrashRequest(file.uploadedFileID!, file.collectionID!));
  157. }
  158. try {
  159. await TrashSyncService.instance.trashFilesOnServer(trashRequests);
  160. await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs);
  161. } catch (e, s) {
  162. _logger.severe("Failed to delete files from remote", e, s);
  163. showGenericErrorDialog(context: context);
  164. rethrow;
  165. }
  166. for (final collectionID in updatedCollectionIDs) {
  167. Bus.instance.fire(
  168. CollectionUpdatedEvent(
  169. collectionID,
  170. files.where((file) => file.collectionID == collectionID).toList(),
  171. "deleteFromRemoteOnly",
  172. type: EventType.deletedFromRemote,
  173. ),
  174. );
  175. }
  176. Bus.instance.fire(
  177. LocalPhotosUpdatedEvent(
  178. files,
  179. type: EventType.deletedFromRemote,
  180. source: "deleteFromRemoteOnly",
  181. ),
  182. );
  183. SyncService.instance.sync();
  184. RemoteSyncService.instance.sync(silently: true);
  185. }
  186. Future<void> deleteFilesOnDeviceOnly(
  187. BuildContext context,
  188. List<EnteFile> files,
  189. ) async {
  190. _logger.info("Trying to deleteFilesOnDeviceOnly" + files.toString());
  191. final List<String> localAssetIDs = [];
  192. final List<String> localSharedMediaIDs = [];
  193. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  194. bool hasLocalOnlyFiles = false;
  195. for (final file in files) {
  196. if (file.localID != null) {
  197. if (!(await _localFileExist(file))) {
  198. _logger.warning("Already deleted " + file.toString());
  199. alreadyDeletedIDs.add(file.localID!);
  200. } else if (file.isSharedMediaToAppSandbox) {
  201. localSharedMediaIDs.add(file.localID!);
  202. } else {
  203. localAssetIDs.add(file.localID!);
  204. }
  205. }
  206. if (file.uploadedFileID == null) {
  207. hasLocalOnlyFiles = true;
  208. }
  209. }
  210. if (hasLocalOnlyFiles && Platform.isAndroid) {
  211. final shouldProceed = await shouldProceedWithDeletion(context);
  212. if (!shouldProceed) {
  213. return;
  214. }
  215. }
  216. Set<String> deletedIDs = <String>{};
  217. try {
  218. deletedIDs =
  219. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  220. } catch (e, s) {
  221. _logger.severe("Could not delete file", e, s);
  222. }
  223. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  224. final List<EnteFile> deletedFiles = [];
  225. for (final file in files) {
  226. // Remove only those files that have been removed from disk
  227. if (deletedIDs.contains(file.localID) ||
  228. alreadyDeletedIDs.contains(file.localID)) {
  229. deletedFiles.add(file);
  230. file.localID = null;
  231. FilesDB.instance.update(file);
  232. }
  233. }
  234. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  235. Bus.instance.fire(
  236. LocalPhotosUpdatedEvent(
  237. deletedFiles,
  238. type: EventType.deletedFromDevice,
  239. source: "deleteFilesOnDeviceOnly",
  240. ),
  241. );
  242. }
  243. }
  244. Future<bool> deleteFromTrash(BuildContext context, List<EnteFile> files) async {
  245. bool didDeletionStart = false;
  246. final actionResult = await showChoiceActionSheet(
  247. context,
  248. title: S.of(context).permanentlyDelete,
  249. body: S.of(context).thisActionCannotBeUndone,
  250. firstButtonLabel: S.of(context).delete,
  251. isCritical: true,
  252. firstButtonOnTap: () async {
  253. try {
  254. didDeletionStart = true;
  255. await TrashSyncService.instance.deleteFromTrash(files);
  256. Bus.instance.fire(
  257. FilesUpdatedEvent(
  258. files,
  259. type: EventType.deletedFromEverywhere,
  260. source: "deleteFromTrash",
  261. ),
  262. );
  263. //the FilesUpdateEvent is not reloading trash on premanently removing
  264. //files, so need to fire ForceReloadTrashPageEvent
  265. Bus.instance.fire(ForceReloadTrashPageEvent());
  266. } catch (e, s) {
  267. _logger.info("failed to delete from trash", e, s);
  268. rethrow;
  269. }
  270. },
  271. );
  272. if (actionResult?.action == null ||
  273. actionResult!.action == ButtonAction.cancel) {
  274. return didDeletionStart ? true : false;
  275. } else if (actionResult.action == ButtonAction.error) {
  276. await showGenericErrorDialog(context: context);
  277. return false;
  278. } else {
  279. return true;
  280. }
  281. }
  282. Future<bool> emptyTrash(BuildContext context) async {
  283. final actionResult = await showChoiceActionSheet(
  284. context,
  285. title: S.of(context).emptyTrash,
  286. body: S.of(context).permDeleteWarning,
  287. firstButtonLabel: S.of(context).empty,
  288. isCritical: true,
  289. firstButtonOnTap: () async {
  290. try {
  291. await TrashSyncService.instance.emptyTrash();
  292. } catch (e, s) {
  293. _logger.info("failed empty trash", e, s);
  294. rethrow;
  295. }
  296. },
  297. );
  298. if (actionResult?.action == null ||
  299. actionResult!.action == ButtonAction.cancel) {
  300. return false;
  301. } else if (actionResult.action == ButtonAction.error) {
  302. await showGenericErrorDialog(context: context);
  303. return false;
  304. } else {
  305. return true;
  306. }
  307. }
  308. Future<bool> deleteLocalFiles(
  309. BuildContext context,
  310. List<String> localIDs,
  311. ) async {
  312. final List<String> deletedIDs = [];
  313. final List<String> localAssetIDs = [];
  314. final List<String> localSharedMediaIDs = [];
  315. for (String id in localIDs) {
  316. if (id.startsWith(oldSharedMediaIdentifier) ||
  317. id.startsWith(sharedMediaIdentifier)) {
  318. localSharedMediaIDs.add(id);
  319. } else {
  320. localAssetIDs.add(id);
  321. }
  322. }
  323. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  324. final bool shouldDeleteInBatches =
  325. await isAndroidSDKVersionLowerThan(android11SDKINT);
  326. if (shouldDeleteInBatches) {
  327. deletedIDs.addAll(await deleteLocalFilesInBatches(context, localAssetIDs));
  328. } else {
  329. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  330. }
  331. // In IOS, the library returns no error and fail to delete any file is
  332. // there's any shared file. As a stop-gap solution, we initiate deletion in
  333. // batches. Similar in Android, for large number of files, we have observed
  334. // that the library fails to delete any file. So, we initiate deletion in
  335. // batches.
  336. if (deletedIDs.isEmpty) {
  337. deletedIDs.addAll(
  338. await deleteLocalFilesInBatches(
  339. context,
  340. localAssetIDs,
  341. maximumBatchSize: 1000,
  342. minimumBatchSize: 10,
  343. ),
  344. );
  345. _logger
  346. .severe("iOS free-space fallback, deleted ${deletedIDs.length} files "
  347. "in batches}");
  348. }
  349. if (deletedIDs.isNotEmpty) {
  350. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  351. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  352. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  353. Bus.instance.fire(
  354. LocalPhotosUpdatedEvent(deletedFiles, source: "deleteLocal"),
  355. );
  356. return true;
  357. } else {
  358. showToast(context, S.of(context).couldNotFreeUpSpace);
  359. return false;
  360. }
  361. }
  362. Future<List<String>> _deleteLocalFilesInOneShot(
  363. BuildContext context,
  364. List<String> localIDs,
  365. ) async {
  366. _logger.info('starting _deleteLocalFilesInOneShot for ${localIDs.length}');
  367. final List<String> deletedIDs = [];
  368. final dialog = createProgressDialog(
  369. context,
  370. "Deleting " + localIDs.length.toString() + " backed up files...",
  371. );
  372. await dialog.show();
  373. try {
  374. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  375. } catch (e, s) {
  376. _logger.severe("Could not delete files ", e, s);
  377. }
  378. _logger.info(
  379. '_deleteLocalFilesInOneShot deleted ${deletedIDs.length} out '
  380. 'of ${localIDs.length}',
  381. );
  382. await dialog.hide();
  383. return deletedIDs;
  384. }
  385. Future<List<String>> deleteLocalFilesInBatches(
  386. BuildContext context,
  387. List<String> localIDs, {
  388. int minimumParts = 10,
  389. int minimumBatchSize = 1,
  390. int maximumBatchSize = 100,
  391. }) async {
  392. final dialogKey = GlobalKey<LinearProgressDialogState>();
  393. final dialog = LinearProgressDialog(
  394. "Deleting " + localIDs.length.toString() + " backed up files...",
  395. key: dialogKey,
  396. );
  397. showDialog(
  398. context: context,
  399. builder: (context) {
  400. return dialog;
  401. },
  402. barrierColor: Colors.black.withOpacity(0.85),
  403. );
  404. final batchSize = min(
  405. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  406. maximumBatchSize,
  407. );
  408. final List<String> deletedIDs = [];
  409. for (int index = 0; index < localIDs.length; index += batchSize) {
  410. if (dialogKey.currentState != null) {
  411. dialogKey.currentState!.setProgress(index / localIDs.length);
  412. }
  413. final ids = localIDs
  414. .getRange(index, min(localIDs.length, index + batchSize))
  415. .toList();
  416. _logger.info("Trying to delete " + ids.toString());
  417. try {
  418. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  419. _logger.info("Deleted " + ids.toString());
  420. } catch (e, s) {
  421. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  422. for (final id in ids) {
  423. try {
  424. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  425. _logger.info("Deleted " + id);
  426. } catch (e, s) {
  427. _logger.severe("Could not delete file " + id, e, s);
  428. }
  429. }
  430. }
  431. }
  432. Navigator.of(dialogKey.currentContext!, rootNavigator: true).pop('dialog');
  433. return deletedIDs;
  434. }
  435. Future<bool> _localFileExist(EnteFile file) {
  436. if (file.isSharedMediaToAppSandbox) {
  437. final localFile = File(getSharedMediaFilePath(file));
  438. return localFile.exists();
  439. } else {
  440. return file.getAsset.then((asset) {
  441. if (asset == null) {
  442. return false;
  443. }
  444. return asset.exists;
  445. });
  446. }
  447. }
  448. Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
  449. final List<String> actuallyDeletedIDs = [];
  450. try {
  451. return Future.forEach<String>(localIDs, (id) async {
  452. final String localPath = getSharedMediaPathFromLocalID(id);
  453. try {
  454. // verify the file exists as the OS may have already deleted it from cache
  455. if (File(localPath).existsSync()) {
  456. await File(localPath).delete();
  457. }
  458. actuallyDeletedIDs.add(id);
  459. } catch (e, s) {
  460. _logger.warning("Could not delete file " + id, e, s);
  461. // server log shouldn't contain localId
  462. _logger.severe("Could not delete file ", e, s);
  463. }
  464. }).then((ignore) {
  465. return actuallyDeletedIDs;
  466. });
  467. } catch (e, s) {
  468. _logger.severe("Unexpected error while deleting share media files", e, s);
  469. return Future.value(actuallyDeletedIDs);
  470. }
  471. }
  472. Future<bool> shouldProceedWithDeletion(BuildContext context) async {
  473. final actionResult = await showChoiceActionSheet(
  474. context,
  475. title: S.of(context).permanentlyDeleteFromDevice,
  476. body: S.of(context).someOfTheFilesYouAreTryingToDeleteAre,
  477. firstButtonLabel: S.of(context).delete,
  478. isCritical: true,
  479. );
  480. if (actionResult?.action == null) {
  481. return false;
  482. } else {
  483. return actionResult!.action == ButtonAction.first;
  484. }
  485. }
  486. Future<void> showDeleteSheet(
  487. BuildContext context,
  488. SelectedFiles selectedFiles,
  489. FilesSplit filesSplit,
  490. ) async {
  491. if (selectedFiles.files.length != filesSplit.count) {
  492. throw AssertionError("Unexpected state, #{selectedFiles.files.length} != "
  493. "${filesSplit.count}");
  494. }
  495. final List<EnteFile> deletableFiles =
  496. filesSplit.ownedByCurrentUser + filesSplit.pendingUploads;
  497. if (deletableFiles.isEmpty && filesSplit.ownedByOtherUsers.isNotEmpty) {
  498. showShortToast(context, S.of(context).cannotDeleteSharedFiles);
  499. return;
  500. }
  501. final containsUploadedFile = deletableFiles.any((f) => f.isUploaded);
  502. final containsLocalFile = deletableFiles.any((f) => f.localID != null);
  503. final List<ButtonWidget> buttons = [];
  504. final bool isBothLocalAndRemote = containsUploadedFile && containsLocalFile;
  505. final bool isLocalOnly = !containsUploadedFile;
  506. final bool isRemoteOnly = !containsLocalFile;
  507. final String? bodyHighlight = isBothLocalAndRemote
  508. ? S.of(context).theyWillBeDeletedFromAllAlbums
  509. : null;
  510. String body = "";
  511. if (isBothLocalAndRemote) {
  512. body = S.of(context).someItemsAreInBothEnteAndYourDevice;
  513. } else if (isRemoteOnly) {
  514. body = S.of(context).selectedItemsWillBeDeletedFromAllAlbumsAndMoved;
  515. } else if (isLocalOnly) {
  516. body = S.of(context).theseItemsWillBeDeletedFromYourDevice;
  517. } else {
  518. throw AssertionError("Unexpected state");
  519. }
  520. // Add option to delete from ente
  521. if (isBothLocalAndRemote || isRemoteOnly) {
  522. buttons.add(
  523. ButtonWidget(
  524. labelText: isBothLocalAndRemote
  525. ? S.of(context).deleteFromEnte
  526. : S.of(context).yesDelete,
  527. buttonType: ButtonType.neutral,
  528. buttonSize: ButtonSize.large,
  529. shouldStickToDarkTheme: true,
  530. buttonAction: ButtonAction.first,
  531. shouldSurfaceExecutionStates: true,
  532. isInAlert: true,
  533. onTap: () async {
  534. await deleteFilesFromRemoteOnly(
  535. context,
  536. deletableFiles,
  537. ).then(
  538. (value) {
  539. showShortToast(context, S.of(context).movedToTrash);
  540. },
  541. onError: (e, s) {
  542. showGenericErrorDialog(context: context);
  543. },
  544. );
  545. },
  546. ),
  547. );
  548. }
  549. // Add option to delete from local
  550. if (isBothLocalAndRemote || isLocalOnly) {
  551. buttons.add(
  552. ButtonWidget(
  553. labelText: isBothLocalAndRemote
  554. ? S.of(context).deleteFromDevice
  555. : S.of(context).yesDelete,
  556. buttonType: ButtonType.neutral,
  557. buttonSize: ButtonSize.large,
  558. shouldStickToDarkTheme: true,
  559. buttonAction: ButtonAction.second,
  560. shouldSurfaceExecutionStates: false,
  561. isInAlert: true,
  562. onTap: () async {
  563. await deleteFilesOnDeviceOnly(context, deletableFiles);
  564. },
  565. ),
  566. );
  567. }
  568. if (isBothLocalAndRemote) {
  569. buttons.add(
  570. ButtonWidget(
  571. labelText: S.of(context).deleteFromBoth,
  572. buttonType: ButtonType.neutral,
  573. buttonSize: ButtonSize.large,
  574. shouldStickToDarkTheme: true,
  575. buttonAction: ButtonAction.third,
  576. shouldSurfaceExecutionStates: true,
  577. isInAlert: true,
  578. onTap: () async {
  579. await deleteFilesFromEverywhere(
  580. context,
  581. deletableFiles,
  582. );
  583. },
  584. ),
  585. );
  586. }
  587. buttons.add(
  588. ButtonWidget(
  589. labelText: S.of(context).cancel,
  590. buttonType: ButtonType.secondary,
  591. buttonSize: ButtonSize.large,
  592. shouldStickToDarkTheme: true,
  593. buttonAction: ButtonAction.fourth,
  594. isInAlert: true,
  595. ),
  596. );
  597. final actionResult = await showActionSheet(
  598. context: context,
  599. buttons: buttons,
  600. actionSheetType: ActionSheetType.defaultActionSheet,
  601. body: body,
  602. bodyHighlight: bodyHighlight,
  603. );
  604. if (actionResult?.action != null &&
  605. actionResult!.action == ButtonAction.error) {
  606. showGenericErrorDialog(context: context);
  607. } else {
  608. selectedFiles.clearAll();
  609. }
  610. }