delete_file_util.dart 20 KB

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