file_selection_actions_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import 'package:fast_base58/fast_base58.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:photos/core/configuration.dart';
  5. import 'package:photos/models/collection.dart';
  6. import 'package:photos/models/device_collection.dart';
  7. import 'package:photos/models/file.dart';
  8. import 'package:photos/models/files_split.dart';
  9. import 'package:photos/models/gallery_type.dart';
  10. import 'package:photos/models/magic_metadata.dart';
  11. import 'package:photos/models/selected_files.dart';
  12. import 'package:photos/services/collections_service.dart';
  13. import 'package:photos/services/hidden_service.dart';
  14. import 'package:photos/theme/ente_theme.dart';
  15. import 'package:photos/ui/actions/collection/collection_file_actions.dart';
  16. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  17. import 'package:photos/ui/components/action_sheet_widget.dart';
  18. import 'package:photos/ui/components/blur_menu_item_widget.dart';
  19. import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
  20. import 'package:photos/ui/components/button_widget.dart';
  21. import 'package:photos/ui/components/models/button_type.dart';
  22. import 'package:photos/ui/create_collection_sheet.dart';
  23. import 'package:photos/ui/sharing/manage_links_widget.dart';
  24. import 'package:photos/utils/delete_file_util.dart';
  25. import 'package:photos/utils/magic_util.dart';
  26. import 'package:photos/utils/navigation_util.dart';
  27. import 'package:photos/utils/toast_util.dart';
  28. class FileSelectionActionWidget extends StatefulWidget {
  29. final GalleryType type;
  30. final Collection? collection;
  31. final DeviceCollection? deviceCollection;
  32. final SelectedFiles selectedFiles;
  33. const FileSelectionActionWidget(
  34. this.type,
  35. this.selectedFiles, {
  36. Key? key,
  37. this.collection,
  38. this.deviceCollection,
  39. }) : super(key: key);
  40. @override
  41. State<FileSelectionActionWidget> createState() =>
  42. _FileSelectionActionWidgetState();
  43. }
  44. class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
  45. late int currentUserID;
  46. late FilesSplit split;
  47. late CollectionActions collectionActions;
  48. late bool isCollectionOwner;
  49. // _cachedCollectionForSharedLink is primarly used to avoid creating duplicate
  50. // links if user keeps on creating Create link button after selecting
  51. // few files. This link is reset on any selection changed;
  52. Collection? _cachedCollectionForSharedLink;
  53. @override
  54. void initState() {
  55. currentUserID = Configuration.instance.getUserID()!;
  56. split = FilesSplit.split(<File>[], currentUserID);
  57. widget.selectedFiles.addListener(_selectFileChangeListener);
  58. collectionActions = CollectionActions(CollectionsService.instance);
  59. isCollectionOwner =
  60. widget.collection != null && widget.collection!.isOwner(currentUserID);
  61. super.initState();
  62. }
  63. @override
  64. void dispose() {
  65. widget.selectedFiles.removeListener(_selectFileChangeListener);
  66. super.dispose();
  67. }
  68. void _selectFileChangeListener() {
  69. if (_cachedCollectionForSharedLink != null) {
  70. _cachedCollectionForSharedLink = null;
  71. }
  72. split = FilesSplit.split(widget.selectedFiles.files, currentUserID);
  73. if (mounted) {
  74. setState(() => {});
  75. }
  76. }
  77. @override
  78. Widget build(BuildContext context) {
  79. final bool showPrefix =
  80. split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty;
  81. final String suffix = showPrefix
  82. ? " (${split.ownedByCurrentUser.length})"
  83. ""
  84. : "";
  85. final int removeCount = split.ownedByCurrentUser.length +
  86. (isCollectionOwner ? split.ownedByOtherUsers.length : 0);
  87. final String removeSuffix = showPrefix
  88. ? " ($removeCount)"
  89. ""
  90. : "";
  91. final String suffixInPending = split.ownedByOtherUsers.isNotEmpty
  92. ? " (${split.ownedByCurrentUser.length + split.pendingUploads.length})"
  93. ""
  94. : "";
  95. final bool anyOwnedFiles =
  96. split.pendingUploads.isNotEmpty || split.ownedByCurrentUser.isNotEmpty;
  97. final bool anyUploadedFiles = split.ownedByCurrentUser.isNotEmpty;
  98. final bool showRemoveOption = widget.type.showRemoveFromAlbum();
  99. debugPrint('$runtimeType building $mounted');
  100. final colorScheme = getEnteColorScheme(context);
  101. final List<List<BlurMenuItemWidget>> items = [];
  102. final List<BlurMenuItemWidget> firstList = [];
  103. final List<BlurMenuItemWidget> secondList = [];
  104. if (widget.type.showCreateLink()) {
  105. if (_cachedCollectionForSharedLink != null && anyUploadedFiles) {
  106. firstList.add(
  107. BlurMenuItemWidget(
  108. leadingIcon: Icons.copy_outlined,
  109. labelText: "Copy link",
  110. menuItemColor: colorScheme.fillFaint,
  111. onTap: anyUploadedFiles ? _copyLink : null,
  112. ),
  113. );
  114. } else {
  115. firstList.add(
  116. BlurMenuItemWidget(
  117. leadingIcon: Icons.link_outlined,
  118. labelText: "Create link$suffix",
  119. menuItemColor: colorScheme.fillFaint,
  120. onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null,
  121. ),
  122. );
  123. }
  124. }
  125. final showUploadIcon = widget.type == GalleryType.localFolder &&
  126. split.ownedByCurrentUser.isEmpty;
  127. if (widget.type.showAddToAlbum()) {
  128. secondList.add(
  129. BlurMenuItemWidget(
  130. leadingIcon:
  131. showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
  132. labelText:
  133. "Add to ${showUploadIcon ? 'ente' : 'album'}$suffixInPending",
  134. menuItemColor: colorScheme.fillFaint,
  135. onTap: anyOwnedFiles ? _addToAlbum : null,
  136. ),
  137. );
  138. }
  139. if (widget.type.showMoveToAlbum()) {
  140. secondList.add(
  141. BlurMenuItemWidget(
  142. leadingIcon: Icons.arrow_forward_outlined,
  143. labelText: "Move to album$suffix",
  144. menuItemColor: colorScheme.fillFaint,
  145. onTap: anyUploadedFiles ? _moveFiles : null,
  146. ),
  147. );
  148. }
  149. if (showRemoveOption) {
  150. secondList.add(
  151. BlurMenuItemWidget(
  152. leadingIcon: Icons.remove_outlined,
  153. labelText: "Remove from album$removeSuffix",
  154. menuItemColor: colorScheme.fillFaint,
  155. onTap: removeCount > 0 ? _removeFilesFromAlbum : null,
  156. ),
  157. );
  158. }
  159. if (widget.type.showDeleteOption()) {
  160. secondList.add(
  161. BlurMenuItemWidget(
  162. leadingIcon: Icons.delete_outline,
  163. labelText: "Delete$suffixInPending",
  164. menuItemColor: colorScheme.fillFaint,
  165. onTap: anyOwnedFiles ? _onDeleteClick : null,
  166. ),
  167. );
  168. }
  169. if (widget.type.showHideOption()) {
  170. secondList.add(
  171. BlurMenuItemWidget(
  172. leadingIcon: Icons.visibility_off_outlined,
  173. labelText: "Hide$suffix",
  174. menuItemColor: colorScheme.fillFaint,
  175. onTap: anyUploadedFiles ? _onHideClick : null,
  176. ),
  177. );
  178. } else if (widget.type.showUnHideOption()) {
  179. secondList.add(
  180. BlurMenuItemWidget(
  181. leadingIcon: Icons.visibility_off_outlined,
  182. labelText: "Unhide$suffix",
  183. menuItemColor: colorScheme.fillFaint,
  184. onTap: _onUnhideClick,
  185. ),
  186. );
  187. }
  188. if (widget.type.showArchiveOption()) {
  189. secondList.add(
  190. BlurMenuItemWidget(
  191. leadingIcon: Icons.archive_outlined,
  192. labelText: "Archive$suffix",
  193. menuItemColor: colorScheme.fillFaint,
  194. onTap: anyUploadedFiles ? _onArchiveClick : null,
  195. ),
  196. );
  197. } else if (widget.type.showUnArchiveOption()) {
  198. secondList.add(
  199. BlurMenuItemWidget(
  200. leadingIcon: Icons.unarchive,
  201. labelText: "Unarchive$suffix",
  202. menuItemColor: colorScheme.fillFaint,
  203. onTap: _onUnArchiveClick,
  204. ),
  205. );
  206. }
  207. if (widget.type.showFavoriteOption()) {
  208. secondList.add(
  209. BlurMenuItemWidget(
  210. leadingIcon: Icons.favorite_border_rounded,
  211. labelText: "Favorite$suffix",
  212. menuItemColor: colorScheme.fillFaint,
  213. onTap: anyUploadedFiles ? _onFavoriteClick : null,
  214. ),
  215. );
  216. } else if (widget.type.showUnFavoriteOption()) {
  217. secondList.add(
  218. BlurMenuItemWidget(
  219. leadingIcon: Icons.favorite,
  220. labelText: "Remove from favorite$suffix",
  221. menuItemColor: colorScheme.fillFaint,
  222. onTap: _onUnFavoriteClick,
  223. ),
  224. );
  225. }
  226. if (widget.type.showRestoreOption()) {
  227. secondList.add(
  228. BlurMenuItemWidget(
  229. leadingIcon: Icons.visibility,
  230. labelText: "Restore",
  231. menuItemColor: colorScheme.fillFaint,
  232. onTap: _restore,
  233. ),
  234. );
  235. }
  236. if (widget.type.showPermanentlyDeleteOption()) {
  237. secondList.add(
  238. BlurMenuItemWidget(
  239. leadingIcon: Icons.delete_forever_outlined,
  240. labelText: "Permanently delete",
  241. menuItemColor: colorScheme.fillFaint,
  242. onTap: _permanentlyDelete,
  243. ),
  244. );
  245. }
  246. if (firstList.isNotEmpty || secondList.isNotEmpty) {
  247. if (firstList.isNotEmpty) {
  248. items.add(firstList);
  249. }
  250. items.add(secondList);
  251. return ExpandedMenuWidget(
  252. items: items,
  253. );
  254. } else {
  255. // TODO: Return "Select All" here
  256. return const SizedBox.shrink();
  257. }
  258. }
  259. Future<void> _moveFiles() async {
  260. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  261. widget.selectedFiles
  262. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  263. widget.selectedFiles
  264. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  265. }
  266. createCollectionSheet(
  267. widget.selectedFiles,
  268. null,
  269. context,
  270. actionType: CollectionActionType.moveFiles,
  271. );
  272. }
  273. Future<void> _addToAlbum() async {
  274. if (split.ownedByOtherUsers.isNotEmpty) {
  275. widget.selectedFiles
  276. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  277. }
  278. createCollectionSheet(
  279. widget.selectedFiles,
  280. null,
  281. context,
  282. );
  283. }
  284. Future<void> _onDeleteClick() async {
  285. return showDeleteSheet(context, widget.selectedFiles);
  286. }
  287. Future<void> _removeFilesFromAlbum() async {
  288. if (split.pendingUploads.isNotEmpty) {
  289. widget.selectedFiles
  290. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  291. }
  292. if (!isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) {
  293. widget.selectedFiles
  294. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  295. }
  296. final bool removingOthersFile =
  297. isCollectionOwner && split.ownedByOtherUsers.isNotEmpty;
  298. await collectionActions.showRemoveFromCollectionSheetV2(
  299. context,
  300. widget.collection!,
  301. widget.selectedFiles,
  302. removingOthersFile,
  303. );
  304. }
  305. Future<void> _onFavoriteClick() async {
  306. final result = await collectionActions.updateFavorites(
  307. context,
  308. split.ownedByCurrentUser,
  309. true,
  310. );
  311. if (result) {
  312. widget.selectedFiles.clearAll();
  313. }
  314. }
  315. Future<void> _onUnFavoriteClick() async {
  316. final result = await collectionActions.updateFavorites(
  317. context,
  318. split.ownedByCurrentUser,
  319. false,
  320. );
  321. if (result) {
  322. widget.selectedFiles.clearAll();
  323. }
  324. }
  325. Future<void> _onArchiveClick() async {
  326. await changeVisibility(
  327. context,
  328. split.ownedByCurrentUser,
  329. visibilityArchive,
  330. );
  331. widget.selectedFiles.clearAll();
  332. }
  333. Future<void> _onUnArchiveClick() async {
  334. await changeVisibility(
  335. context,
  336. split.ownedByCurrentUser,
  337. visibilityVisible,
  338. );
  339. widget.selectedFiles.clearAll();
  340. }
  341. Future<void> _onHideClick() async {
  342. await CollectionsService.instance.hideFiles(
  343. context,
  344. split.ownedByCurrentUser,
  345. );
  346. widget.selectedFiles.clearAll();
  347. }
  348. Future<void> _onUnhideClick() async {
  349. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  350. widget.selectedFiles
  351. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  352. widget.selectedFiles
  353. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  354. }
  355. createCollectionSheet(
  356. widget.selectedFiles,
  357. null,
  358. context,
  359. actionType: CollectionActionType.unHide,
  360. );
  361. }
  362. Future<void> _onCreatedSharedLinkClicked() async {
  363. if (split.ownedByCurrentUser.isEmpty) {
  364. showShortToast(context, "Can only create link for files owned by you");
  365. return;
  366. }
  367. _cachedCollectionForSharedLink ??= await collectionActions
  368. .createSharedCollectionLink(context, split.ownedByCurrentUser);
  369. final actionResult = await showActionSheet(
  370. context: context,
  371. buttons: [
  372. const ButtonWidget(
  373. labelText: "Copy link",
  374. buttonType: ButtonType.neutral,
  375. buttonSize: ButtonSize.large,
  376. shouldStickToDarkTheme: true,
  377. buttonAction: ButtonAction.first,
  378. isInAlert: true,
  379. ),
  380. const ButtonWidget(
  381. labelText: "Manage link",
  382. buttonType: ButtonType.secondary,
  383. buttonSize: ButtonSize.large,
  384. buttonAction: ButtonAction.second,
  385. shouldStickToDarkTheme: true,
  386. isInAlert: true,
  387. ),
  388. const ButtonWidget(
  389. labelText: "Done",
  390. buttonType: ButtonType.secondary,
  391. buttonSize: ButtonSize.large,
  392. buttonAction: ButtonAction.third,
  393. shouldStickToDarkTheme: true,
  394. isInAlert: true,
  395. )
  396. ],
  397. title: "Public link created",
  398. body: "You can manage your links in the share tab.",
  399. actionSheetType: ActionSheetType.defaultActionSheet,
  400. );
  401. if (actionResult?.action != null) {
  402. if (actionResult!.action == ButtonAction.first) {
  403. await _copyLink();
  404. }
  405. if (actionResult.action == ButtonAction.second) {
  406. routeToPage(
  407. context,
  408. ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink),
  409. );
  410. }
  411. }
  412. if (mounted) {
  413. setState(() => {});
  414. }
  415. }
  416. Future<void> _copyLink() async {
  417. if (_cachedCollectionForSharedLink != null) {
  418. final String collectionKey = Base58Encode(
  419. CollectionsService.instance
  420. .getCollectionKey(_cachedCollectionForSharedLink!.id),
  421. );
  422. final String url =
  423. "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
  424. await Clipboard.setData(ClipboardData(text: url));
  425. showShortToast(context, "Link copied to clipboard");
  426. }
  427. }
  428. void _restore() {
  429. createCollectionSheet(
  430. widget.selectedFiles,
  431. null,
  432. context,
  433. actionType: CollectionActionType.restoreFiles,
  434. );
  435. }
  436. Future<void> _permanentlyDelete() async {
  437. if (await deleteFromTrash(
  438. context,
  439. widget.selectedFiles.files.toList(),
  440. )) {
  441. widget.selectedFiles.clearAll();
  442. }
  443. }
  444. }