file_selection_actions_widget.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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/generated/l10n.dart";
  6. import 'package:photos/models/collection.dart';
  7. import 'package:photos/models/device_collection.dart';
  8. import 'package:photos/models/file.dart';
  9. import "package:photos/models/file_type.dart";
  10. import 'package:photos/models/files_split.dart';
  11. import 'package:photos/models/gallery_type.dart';
  12. import "package:photos/models/metadata/common_keys.dart";
  13. import 'package:photos/models/selected_files.dart';
  14. import 'package:photos/services/collections_service.dart';
  15. import 'package:photos/services/hidden_service.dart';
  16. import 'package:photos/ui/actions/collection/collection_file_actions.dart';
  17. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  18. import 'package:photos/ui/collections/collection_action_sheet.dart';
  19. import 'package:photos/ui/components/action_sheet_widget.dart';
  20. import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart";
  21. import 'package:photos/ui/components/buttons/button_widget.dart';
  22. import 'package:photos/ui/components/models/button_type.dart';
  23. import 'package:photos/ui/sharing/manage_links_widget.dart';
  24. import "package:photos/ui/tools/collage/collage_creator_page.dart";
  25. import 'package:photos/utils/delete_file_util.dart';
  26. import 'package:photos/utils/magic_util.dart';
  27. import 'package:photos/utils/navigation_util.dart';
  28. import "package:photos/utils/share_util.dart";
  29. import 'package:photos/utils/toast_util.dart';
  30. class FileSelectionActionsWidget extends StatefulWidget {
  31. final GalleryType type;
  32. final Collection? collection;
  33. final DeviceCollection? deviceCollection;
  34. final SelectedFiles selectedFiles;
  35. const FileSelectionActionsWidget(
  36. this.type,
  37. this.selectedFiles, {
  38. Key? key,
  39. this.collection,
  40. this.deviceCollection,
  41. }) : super(key: key);
  42. @override
  43. State<FileSelectionActionsWidget> createState() =>
  44. _FileSelectionActionsWidgetState();
  45. }
  46. class _FileSelectionActionsWidgetState
  47. extends State<FileSelectionActionsWidget> {
  48. late int currentUserID;
  49. late FilesSplit split;
  50. late CollectionActions collectionActions;
  51. late bool isCollectionOwner;
  52. // _cachedCollectionForSharedLink is primarily used to avoid creating duplicate
  53. // links if user keeps on creating Create link button after selecting
  54. // few files. This link is reset on any selection changed;
  55. Collection? _cachedCollectionForSharedLink;
  56. final GlobalKey shareButtonKey = GlobalKey();
  57. @override
  58. void initState() {
  59. currentUserID = Configuration.instance.getUserID()!;
  60. split = FilesSplit.split(<File>[], currentUserID);
  61. widget.selectedFiles.addListener(_selectFileChangeListener);
  62. collectionActions = CollectionActions(CollectionsService.instance);
  63. isCollectionOwner =
  64. widget.collection != null && widget.collection!.isOwner(currentUserID);
  65. super.initState();
  66. }
  67. @override
  68. void dispose() {
  69. widget.selectedFiles.removeListener(_selectFileChangeListener);
  70. super.dispose();
  71. }
  72. void _selectFileChangeListener() {
  73. if (_cachedCollectionForSharedLink != null) {
  74. _cachedCollectionForSharedLink = null;
  75. }
  76. split = FilesSplit.split(widget.selectedFiles.files, currentUserID);
  77. if (mounted) {
  78. setState(() => {});
  79. }
  80. }
  81. @override
  82. Widget build(BuildContext context) {
  83. final bool showPrefix =
  84. split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty;
  85. final String suffix = showPrefix
  86. ? " (${split.ownedByCurrentUser.length})"
  87. ""
  88. : "";
  89. final int removeCount = split.ownedByCurrentUser.length +
  90. (isCollectionOwner ? split.ownedByOtherUsers.length : 0);
  91. final String removeSuffix = showPrefix
  92. ? " ($removeCount)"
  93. ""
  94. : "";
  95. final String suffixInPending = split.ownedByOtherUsers.isNotEmpty
  96. ? " (${split.ownedByCurrentUser.length + split.pendingUploads.length})"
  97. ""
  98. : "";
  99. final bool anyOwnedFiles =
  100. split.pendingUploads.isNotEmpty || split.ownedByCurrentUser.isNotEmpty;
  101. final bool anyUploadedFiles = split.ownedByCurrentUser.isNotEmpty;
  102. final bool showRemoveOption = widget.type.showRemoveFromAlbum();
  103. debugPrint('$runtimeType building $mounted');
  104. final List<SelectionActionButton> items = [];
  105. if (widget.type.showCreateLink()) {
  106. if (_cachedCollectionForSharedLink != null && anyUploadedFiles) {
  107. items.add(
  108. SelectionActionButton(
  109. icon: Icons.copy_outlined,
  110. labelText: S.of(context).copyLink,
  111. onTap: anyUploadedFiles ? _copyLink : null,
  112. ),
  113. );
  114. } else {
  115. items.add(
  116. SelectionActionButton(
  117. icon: Icons.link_outlined,
  118. labelText: S.of(context).shareLink + suffix,
  119. onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null,
  120. ),
  121. );
  122. }
  123. }
  124. bool hasVideoFile = false;
  125. for (final file in widget.selectedFiles.files) {
  126. if (file.fileType == FileType.video) {
  127. hasVideoFile = true;
  128. }
  129. }
  130. if (!hasVideoFile &&
  131. widget.selectedFiles.files.length >=
  132. CollageCreatorPage.collageItemsMin &&
  133. widget.selectedFiles.files.length <=
  134. CollageCreatorPage.collageItemsMax) {
  135. items.add(
  136. SelectionActionButton(
  137. icon: Icons.grid_view_outlined,
  138. labelText: S.of(context).createCollage,
  139. onTap: _onCreateCollageClicked,
  140. ),
  141. );
  142. }
  143. final showUploadIcon = widget.type == GalleryType.localFolder &&
  144. split.ownedByCurrentUser.isEmpty;
  145. if (widget.type.showAddToAlbum()) {
  146. items.add(
  147. SelectionActionButton(
  148. icon:
  149. showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
  150. labelText: showUploadIcon
  151. ? S.of(context).addToEnte
  152. : S.of(context).addToAlbum + suffixInPending,
  153. onTap: anyOwnedFiles ? _addToAlbum : null,
  154. ),
  155. );
  156. }
  157. if (widget.type.showMoveToAlbum()) {
  158. items.add(
  159. SelectionActionButton(
  160. icon: Icons.arrow_forward_outlined,
  161. labelText: S.of(context).moveToAlbum + suffix,
  162. onTap: anyUploadedFiles ? _moveFiles : null,
  163. ),
  164. );
  165. }
  166. if (showRemoveOption) {
  167. items.add(
  168. SelectionActionButton(
  169. icon: Icons.remove_outlined,
  170. labelText: "${S.of(context).removeFromAlbum}$removeSuffix",
  171. onTap: removeCount > 0 ? _removeFilesFromAlbum : null,
  172. ),
  173. );
  174. }
  175. if (widget.type.showDeleteOption()) {
  176. items.add(
  177. SelectionActionButton(
  178. icon: Icons.delete_outline,
  179. labelText: S.of(context).delete + suffixInPending,
  180. onTap: anyOwnedFiles ? _onDeleteClick : null,
  181. ),
  182. );
  183. }
  184. if (widget.type.showHideOption()) {
  185. items.add(
  186. SelectionActionButton(
  187. icon: Icons.visibility_off_outlined,
  188. labelText: S.of(context).hide + suffix,
  189. onTap: anyUploadedFiles ? _onHideClick : null,
  190. ),
  191. );
  192. } else if (widget.type.showUnHideOption()) {
  193. items.add(
  194. SelectionActionButton(
  195. icon: Icons.visibility_outlined,
  196. labelText: S.of(context).unhide + suffix,
  197. onTap: _onUnhideClick,
  198. ),
  199. );
  200. }
  201. if (widget.type.showArchiveOption()) {
  202. items.add(
  203. SelectionActionButton(
  204. icon: Icons.archive_outlined,
  205. labelText: S.of(context).archive + suffix,
  206. onTap: anyUploadedFiles ? _onArchiveClick : null,
  207. ),
  208. );
  209. } else if (widget.type.showUnArchiveOption()) {
  210. items.add(
  211. SelectionActionButton(
  212. icon: Icons.unarchive,
  213. labelText: S.of(context).unarchive + suffix,
  214. onTap: _onUnArchiveClick,
  215. ),
  216. );
  217. }
  218. if (widget.type.showFavoriteOption()) {
  219. items.add(
  220. SelectionActionButton(
  221. icon: Icons.favorite_border_rounded,
  222. labelText: S.of(context).favorite + suffix,
  223. onTap: anyUploadedFiles ? _onFavoriteClick : null,
  224. ),
  225. );
  226. } else if (widget.type.showUnFavoriteOption()) {
  227. items.add(
  228. SelectionActionButton(
  229. icon: Icons.favorite,
  230. labelText: S.of(context).removeFromFavorite + suffix,
  231. onTap: _onUnFavoriteClick,
  232. ),
  233. );
  234. }
  235. if (widget.type.showRestoreOption()) {
  236. items.add(
  237. SelectionActionButton(
  238. icon: Icons.restore_outlined,
  239. labelText: S.of(context).restore,
  240. onTap: _restore,
  241. ),
  242. );
  243. }
  244. if (widget.type.showPermanentlyDeleteOption()) {
  245. items.add(
  246. SelectionActionButton(
  247. icon: Icons.delete_forever_outlined,
  248. labelText: S.of(context).permanentlyDelete,
  249. onTap: _permanentlyDelete,
  250. ),
  251. );
  252. }
  253. items.add(
  254. SelectionActionButton(
  255. labelText: "Share",
  256. icon: Icons.adaptive.share_outlined,
  257. onTap: () => shareSelected(
  258. context,
  259. shareButtonKey,
  260. widget.selectedFiles.files.toList(),
  261. ),
  262. ),
  263. );
  264. if (items.isNotEmpty) {
  265. return NotificationListener<OverscrollIndicatorNotification>(
  266. onNotification: (overscroll) {
  267. overscroll.disallowIndicator();
  268. return true;
  269. },
  270. child: SingleChildScrollView(
  271. physics: const ClampingScrollPhysics(),
  272. scrollDirection: Axis.horizontal,
  273. child: Row(
  274. crossAxisAlignment: CrossAxisAlignment.start,
  275. children: [
  276. const SizedBox(width: 8),
  277. ...items,
  278. const SizedBox(width: 8),
  279. ],
  280. ),
  281. ),
  282. );
  283. } else {
  284. // TODO: Return "Select All" here
  285. return const SizedBox.shrink();
  286. }
  287. }
  288. Future<void> _moveFiles() async {
  289. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  290. widget.selectedFiles
  291. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  292. widget.selectedFiles
  293. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  294. }
  295. showCollectionActionSheet(
  296. context,
  297. selectedFiles: widget.selectedFiles,
  298. actionType: CollectionActionType.moveFiles,
  299. );
  300. }
  301. Future<void> _addToAlbum() async {
  302. if (split.ownedByOtherUsers.isNotEmpty) {
  303. widget.selectedFiles
  304. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  305. }
  306. showCollectionActionSheet(context, selectedFiles: widget.selectedFiles);
  307. }
  308. Future<void> _onDeleteClick() async {
  309. return showDeleteSheet(context, widget.selectedFiles);
  310. }
  311. Future<void> _removeFilesFromAlbum() async {
  312. if (split.pendingUploads.isNotEmpty) {
  313. widget.selectedFiles
  314. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  315. }
  316. if (!isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) {
  317. widget.selectedFiles
  318. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  319. }
  320. final bool removingOthersFile =
  321. isCollectionOwner && split.ownedByOtherUsers.isNotEmpty;
  322. await collectionActions.showRemoveFromCollectionSheetV2(
  323. context,
  324. widget.collection!,
  325. widget.selectedFiles,
  326. removingOthersFile,
  327. );
  328. }
  329. Future<void> _onFavoriteClick() async {
  330. final result = await collectionActions.updateFavorites(
  331. context,
  332. split.ownedByCurrentUser,
  333. true,
  334. );
  335. if (result) {
  336. widget.selectedFiles.clearAll();
  337. }
  338. }
  339. Future<void> _onUnFavoriteClick() async {
  340. final result = await collectionActions.updateFavorites(
  341. context,
  342. split.ownedByCurrentUser,
  343. false,
  344. );
  345. if (result) {
  346. widget.selectedFiles.clearAll();
  347. }
  348. }
  349. Future<void> _onArchiveClick() async {
  350. await changeVisibility(
  351. context,
  352. split.ownedByCurrentUser,
  353. archiveVisibility,
  354. );
  355. widget.selectedFiles.clearAll();
  356. }
  357. Future<void> _onUnArchiveClick() async {
  358. await changeVisibility(
  359. context,
  360. split.ownedByCurrentUser,
  361. visibleVisibility,
  362. );
  363. widget.selectedFiles.clearAll();
  364. }
  365. Future<void> _onHideClick() async {
  366. await CollectionsService.instance.hideFiles(
  367. context,
  368. split.ownedByCurrentUser,
  369. );
  370. widget.selectedFiles.clearAll();
  371. }
  372. Future<void> _onUnhideClick() async {
  373. if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
  374. widget.selectedFiles
  375. .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
  376. widget.selectedFiles
  377. .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
  378. }
  379. showCollectionActionSheet(
  380. context,
  381. selectedFiles: widget.selectedFiles,
  382. actionType: CollectionActionType.unHide,
  383. );
  384. }
  385. Future<void> _onCreateCollageClicked() async {
  386. final bool? result = await routeToPage(
  387. context,
  388. CollageCreatorPage(widget.selectedFiles.files.toList()),
  389. );
  390. if (result != null && result) {
  391. widget.selectedFiles.clearAll();
  392. }
  393. }
  394. Future<void> _onCreatedSharedLinkClicked() async {
  395. if (split.ownedByCurrentUser.isEmpty) {
  396. showShortToast(
  397. context,
  398. S.of(context).canOnlyCreateLinkForFilesOwnedByYou,
  399. );
  400. return;
  401. }
  402. _cachedCollectionForSharedLink ??= await collectionActions
  403. .createSharedCollectionLink(context, split.ownedByCurrentUser);
  404. final actionResult = await showActionSheet(
  405. context: context,
  406. buttons: [
  407. ButtonWidget(
  408. labelText: S.of(context).copyLink,
  409. buttonType: ButtonType.neutral,
  410. buttonSize: ButtonSize.large,
  411. shouldStickToDarkTheme: true,
  412. buttonAction: ButtonAction.first,
  413. isInAlert: true,
  414. ),
  415. ButtonWidget(
  416. labelText: S.of(context).manageLink,
  417. buttonType: ButtonType.secondary,
  418. buttonSize: ButtonSize.large,
  419. buttonAction: ButtonAction.second,
  420. shouldStickToDarkTheme: true,
  421. isInAlert: true,
  422. ),
  423. ButtonWidget(
  424. labelText: S.of(context).done,
  425. buttonType: ButtonType.secondary,
  426. buttonSize: ButtonSize.large,
  427. buttonAction: ButtonAction.third,
  428. shouldStickToDarkTheme: true,
  429. isInAlert: true,
  430. )
  431. ],
  432. title: S.of(context).publicLinkCreated,
  433. body: S.of(context).youCanManageYourLinksInTheShareTab,
  434. actionSheetType: ActionSheetType.defaultActionSheet,
  435. );
  436. if (actionResult?.action != null) {
  437. if (actionResult!.action == ButtonAction.first) {
  438. await _copyLink();
  439. }
  440. if (actionResult.action == ButtonAction.second) {
  441. routeToPage(
  442. context,
  443. ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink),
  444. );
  445. }
  446. }
  447. if (mounted) {
  448. setState(() => {});
  449. }
  450. }
  451. Future<void> _copyLink() async {
  452. if (_cachedCollectionForSharedLink != null) {
  453. final String collectionKey = Base58Encode(
  454. CollectionsService.instance
  455. .getCollectionKey(_cachedCollectionForSharedLink!.id),
  456. );
  457. final String url =
  458. "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
  459. await Clipboard.setData(ClipboardData(text: url));
  460. showShortToast(context, S.of(context).linkCopiedToClipboard);
  461. }
  462. }
  463. void _restore() {
  464. showCollectionActionSheet(
  465. context,
  466. selectedFiles: widget.selectedFiles,
  467. actionType: CollectionActionType.restoreFiles,
  468. );
  469. }
  470. Future<void> _permanentlyDelete() async {
  471. if (await deleteFromTrash(
  472. context,
  473. widget.selectedFiles.files.toList(),
  474. )) {
  475. widget.selectedFiles.clearAll();
  476. }
  477. }
  478. }