file_selection_actions_widget.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import 'package:fast_base58/fast_base58.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:page_transition/page_transition.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/models/collection.dart';
  7. import 'package:photos/models/device_collection.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_page.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. // _cachedCollectionForSharedLink is primarly used to avoid creating duplicate
  49. // links if user keeps on creating Create link button after selecting
  50. // few files. This link is reset on any selection changed;
  51. Collection? _cachedCollectionForSharedLink;
  52. @override
  53. void initState() {
  54. currentUserID = Configuration.instance.getUserID()!;
  55. split = FilesSplit.split(widget.selectedFiles.files, currentUserID);
  56. widget.selectedFiles.addListener(_selectFileChangeListener);
  57. collectionActions = CollectionActions(CollectionsService.instance);
  58. super.initState();
  59. }
  60. @override
  61. void dispose() {
  62. widget.selectedFiles.removeListener(_selectFileChangeListener);
  63. super.dispose();
  64. }
  65. void _selectFileChangeListener() {
  66. if (_cachedCollectionForSharedLink != null) {
  67. _cachedCollectionForSharedLink = null;
  68. }
  69. split = FilesSplit.split(widget.selectedFiles.files, currentUserID);
  70. if (mounted) {
  71. setState(() => {});
  72. }
  73. }
  74. @override
  75. Widget build(BuildContext context) {
  76. final bool showPrefix =
  77. split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty;
  78. final String suffix = showPrefix
  79. ? " (${split.ownedByCurrentUser.length})"
  80. ""
  81. : "";
  82. final String suffixInPending = split.ownedByOtherUsers.isNotEmpty
  83. ? " (${split.ownedByCurrentUser.length + split.pendingUploads.length})"
  84. ""
  85. : "";
  86. final bool anyOwnedFiles =
  87. split.pendingUploads.isNotEmpty || split.ownedByCurrentUser.isNotEmpty;
  88. final bool anyUploadedFiles = split.ownedByCurrentUser.isNotEmpty;
  89. bool showRemoveOption = widget.type.showRemoveFromAlbum();
  90. if (showRemoveOption && widget.type == GalleryType.sharedCollection) {
  91. showRemoveOption = split.ownedByCurrentUser.isNotEmpty;
  92. }
  93. debugPrint('$runtimeType building $mounted');
  94. final colorScheme = getEnteColorScheme(context);
  95. final List<List<BlurMenuItemWidget>> items = [];
  96. final List<BlurMenuItemWidget> firstList = [];
  97. final List<BlurMenuItemWidget> secondList = [];
  98. if (widget.type.showCreateLink()) {
  99. if (_cachedCollectionForSharedLink != null && anyUploadedFiles) {
  100. firstList.add(
  101. BlurMenuItemWidget(
  102. leadingIcon: Icons.copy_outlined,
  103. labelText: "Copy link",
  104. menuItemColor: colorScheme.fillFaint,
  105. onTap: anyUploadedFiles ? _copyLink : null,
  106. ),
  107. );
  108. } else {
  109. firstList.add(
  110. BlurMenuItemWidget(
  111. leadingIcon: Icons.link_outlined,
  112. labelText: "Create link$suffix",
  113. menuItemColor: colorScheme.fillFaint,
  114. onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null,
  115. ),
  116. );
  117. }
  118. }
  119. final showUploadIcon = widget.type == GalleryType.localFolder &&
  120. split.ownedByCurrentUser.isEmpty;
  121. if (widget.type.showAddToAlbum()) {
  122. secondList.add(
  123. BlurMenuItemWidget(
  124. leadingIcon:
  125. showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
  126. labelText:
  127. "Add to ${showUploadIcon ? 'ente' : 'album'}$suffixInPending",
  128. menuItemColor: colorScheme.fillFaint,
  129. onTap: anyOwnedFiles ? _addToAlbum : null,
  130. ),
  131. );
  132. }
  133. if (widget.type.showMoveToAlbum()) {
  134. secondList.add(
  135. BlurMenuItemWidget(
  136. leadingIcon: Icons.arrow_forward_outlined,
  137. labelText: "Move to album$suffix",
  138. menuItemColor: colorScheme.fillFaint,
  139. onTap: anyUploadedFiles ? _moveFiles : null,
  140. ),
  141. );
  142. }
  143. if (showRemoveOption) {
  144. secondList.add(
  145. BlurMenuItemWidget(
  146. leadingIcon: Icons.remove_outlined,
  147. labelText: "Remove from album$suffix",
  148. menuItemColor: colorScheme.fillFaint,
  149. onTap: anyUploadedFiles ? _removeFilesFromAlbum : null,
  150. ),
  151. );
  152. }
  153. if (widget.type.showDeleteOption()) {
  154. secondList.add(
  155. BlurMenuItemWidget(
  156. leadingIcon: Icons.delete_outline,
  157. labelText: "Delete$suffixInPending",
  158. menuItemColor: colorScheme.fillFaint,
  159. onTap: anyOwnedFiles ? _onDeleteClick : null,
  160. ),
  161. );
  162. }
  163. if (widget.type.showHideOption()) {
  164. secondList.add(
  165. BlurMenuItemWidget(
  166. leadingIcon: Icons.visibility_off_outlined,
  167. labelText: "Hide$suffix",
  168. menuItemColor: colorScheme.fillFaint,
  169. onTap: anyUploadedFiles ? _onHideClick : null,
  170. ),
  171. );
  172. } else if (widget.type.showUnHideOption()) {
  173. secondList.add(
  174. BlurMenuItemWidget(
  175. leadingIcon: Icons.visibility_off_outlined,
  176. labelText: "Unhide$suffix",
  177. menuItemColor: colorScheme.fillFaint,
  178. onTap: _onUnhideClick,
  179. ),
  180. );
  181. }
  182. if (widget.type.showArchiveOption()) {
  183. secondList.add(
  184. BlurMenuItemWidget(
  185. leadingIcon: Icons.archive_outlined,
  186. labelText: "Archive$suffix",
  187. menuItemColor: colorScheme.fillFaint,
  188. onTap: anyUploadedFiles ? _onArchiveClick : null,
  189. ),
  190. );
  191. } else if (widget.type.showUnArchiveOption()) {
  192. secondList.add(
  193. BlurMenuItemWidget(
  194. leadingIcon: Icons.unarchive,
  195. labelText: "Unarchive$suffix",
  196. menuItemColor: colorScheme.fillFaint,
  197. onTap: _onUnArchiveClick,
  198. ),
  199. );
  200. }
  201. if (widget.type.showFavoriteOption()) {
  202. secondList.add(
  203. BlurMenuItemWidget(
  204. leadingIcon: Icons.favorite_border_rounded,
  205. labelText: "Favorite$suffix",
  206. menuItemColor: colorScheme.fillFaint,
  207. onTap: anyUploadedFiles ? _onFavoriteClick : null,
  208. ),
  209. );
  210. } else if (widget.type.showUnFavoriteOption()) {
  211. secondList.add(
  212. BlurMenuItemWidget(
  213. leadingIcon: Icons.favorite,
  214. labelText: "Remove from favorite$suffix",
  215. menuItemColor: colorScheme.fillFaint,
  216. onTap: _onUnFavoriteClick,
  217. ),
  218. );
  219. }
  220. if (firstList.isNotEmpty || secondList.isNotEmpty) {
  221. if (firstList.isNotEmpty) {
  222. items.add(firstList);
  223. }
  224. items.add(secondList);
  225. return ExpandedMenuWidget(
  226. items: items,
  227. );
  228. } else {
  229. // TODO: Return "Select All" here
  230. return const SizedBox.shrink();
  231. }
  232. }
  233. Future<void> _moveFiles() async {
  234. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  235. widget.selectedFiles
  236. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  237. widget.selectedFiles
  238. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  239. }
  240. await _selectionCollectionForAction(CollectionActionType.moveFiles);
  241. }
  242. Future<void> _addToAlbum() async {
  243. if (split.ownedByOtherUsers.isNotEmpty) {
  244. widget.selectedFiles
  245. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  246. }
  247. await _selectionCollectionForAction(CollectionActionType.addFiles);
  248. }
  249. Future<void> _onDeleteClick() async {
  250. showDeleteSheet(context, widget.selectedFiles);
  251. }
  252. Future<void> _removeFilesFromAlbum() async {
  253. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  254. widget.selectedFiles
  255. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  256. widget.selectedFiles
  257. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  258. }
  259. await collectionActions.showRemoveFromCollectionSheet(
  260. context,
  261. widget.collection!,
  262. widget.selectedFiles,
  263. );
  264. }
  265. Future<void> _onFavoriteClick() async {
  266. final result = await collectionActions.updateFavorites(
  267. context,
  268. split.ownedByCurrentUser,
  269. true,
  270. );
  271. if (result) {
  272. widget.selectedFiles.clearAll();
  273. }
  274. }
  275. Future<void> _onUnFavoriteClick() async {
  276. final result = await collectionActions.updateFavorites(
  277. context,
  278. split.ownedByCurrentUser,
  279. false,
  280. );
  281. if (result) {
  282. widget.selectedFiles.clearAll();
  283. }
  284. }
  285. Future<void> _onArchiveClick() async {
  286. await changeVisibility(
  287. context,
  288. split.ownedByCurrentUser,
  289. visibilityArchive,
  290. );
  291. widget.selectedFiles.clearAll();
  292. }
  293. Future<void> _onUnArchiveClick() async {
  294. await changeVisibility(
  295. context,
  296. split.ownedByCurrentUser,
  297. visibilityVisible,
  298. );
  299. widget.selectedFiles.clearAll();
  300. }
  301. Future<void> _onHideClick() async {
  302. await CollectionsService.instance.hideFiles(
  303. context,
  304. split.ownedByCurrentUser,
  305. );
  306. widget.selectedFiles.clearAll();
  307. }
  308. Future<void> _onUnhideClick() async {
  309. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  310. widget.selectedFiles
  311. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  312. widget.selectedFiles
  313. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  314. }
  315. await _selectionCollectionForAction(CollectionActionType.unHide);
  316. }
  317. Future<void> _onCreatedSharedLinkClicked() async {
  318. if (split.ownedByCurrentUser.isEmpty) {
  319. showShortToast(context, "Can only create link for files owned by you");
  320. return;
  321. }
  322. _cachedCollectionForSharedLink ??= await collectionActions
  323. .createSharedCollectionLink(context, split.ownedByCurrentUser);
  324. final actionResult = await showActionSheet(
  325. context: context,
  326. buttons: [
  327. const ButtonWidget(
  328. labelText: "Copy link",
  329. buttonType: ButtonType.neutral,
  330. buttonSize: ButtonSize.large,
  331. shouldStickToDarkTheme: true,
  332. buttonAction: ButtonAction.first,
  333. isInAlert: true,
  334. ),
  335. const ButtonWidget(
  336. labelText: "Manage link",
  337. buttonType: ButtonType.secondary,
  338. buttonSize: ButtonSize.large,
  339. buttonAction: ButtonAction.second,
  340. shouldStickToDarkTheme: true,
  341. isInAlert: true,
  342. ),
  343. const ButtonWidget(
  344. labelText: "Done",
  345. buttonType: ButtonType.secondary,
  346. buttonSize: ButtonSize.large,
  347. buttonAction: ButtonAction.third,
  348. shouldStickToDarkTheme: true,
  349. isInAlert: true,
  350. )
  351. ],
  352. title: "Public link created",
  353. body: "You can manage your links in the share tab.",
  354. actionSheetType: ActionSheetType.defaultActionSheet,
  355. );
  356. if (actionResult != null && actionResult == ButtonAction.first) {
  357. await _copyLink();
  358. }
  359. if (actionResult != null && actionResult == ButtonAction.second) {
  360. routeToPage(
  361. context,
  362. ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink),
  363. );
  364. }
  365. if (mounted) {
  366. setState(() => {});
  367. }
  368. }
  369. Future<void> _copyLink() async {
  370. if (_cachedCollectionForSharedLink != null) {
  371. final String collectionKey = Base58Encode(
  372. CollectionsService.instance
  373. .getCollectionKey(_cachedCollectionForSharedLink!.id),
  374. );
  375. final String url =
  376. "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
  377. await Clipboard.setData(ClipboardData(text: url));
  378. showShortToast(context, "Link copied to clipboard");
  379. }
  380. }
  381. Future<Object?> _selectionCollectionForAction(
  382. CollectionActionType type,
  383. ) async {
  384. return Navigator.push(
  385. context,
  386. PageTransition(
  387. type: PageTransitionType.bottomToTop,
  388. child: CreateCollectionPage(
  389. widget.selectedFiles,
  390. null,
  391. actionType: type,
  392. ),
  393. ),
  394. );
  395. }
  396. }