collections_list_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import "dart:async";
  2. import 'package:flutter/material.dart';
  3. import "package:fluttertoast/fluttertoast.dart";
  4. import 'package:logging/logging.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import "package:photos/core/event_bus.dart";
  7. import 'package:photos/db/files_db.dart';
  8. import "package:photos/events/tab_changed_event.dart";
  9. import "package:photos/generated/l10n.dart";
  10. import 'package:photos/models/collection.dart';
  11. import 'package:photos/models/collection_items.dart';
  12. import 'package:photos/models/file.dart';
  13. import 'package:photos/models/selected_files.dart';
  14. import 'package:photos/services/collections_service.dart';
  15. import 'package:photos/services/ignored_files_service.dart';
  16. import 'package:photos/services/remote_sync_service.dart';
  17. import "package:photos/ui/actions/collection/collection_sharing_actions.dart";
  18. import "package:photos/ui/collection_action_sheet.dart";
  19. import 'package:photos/ui/components/album_list_item_widget.dart';
  20. import 'package:photos/ui/components/new_album_list_widget.dart';
  21. import "package:photos/ui/sharing/share_collection_page.dart";
  22. import 'package:photos/ui/viewer/gallery/collection_page.dart';
  23. import "package:photos/ui/viewer/gallery/empty_state.dart";
  24. import 'package:photos/utils/dialog_util.dart';
  25. import 'package:photos/utils/navigation_util.dart';
  26. import 'package:photos/utils/share_util.dart';
  27. import 'package:photos/utils/toast_util.dart';
  28. import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  29. class CollectionsListWidget extends StatelessWidget {
  30. final List<CollectionWithThumbnail> collectionsWithThumbnail;
  31. final CollectionActionType actionType;
  32. final SelectedFiles? selectedFiles;
  33. final List<SharedMediaFile>? sharedFiles;
  34. final String searchQuery;
  35. final bool shouldShowCreateAlbum;
  36. CollectionsListWidget(
  37. this.collectionsWithThumbnail,
  38. this.actionType,
  39. this.selectedFiles,
  40. this.sharedFiles,
  41. this.searchQuery,
  42. this.shouldShowCreateAlbum, {
  43. Key? key,
  44. }) : super(key: key);
  45. final _logger = Logger("CollectionsListWidgetState");
  46. @override
  47. Widget build(BuildContext context) {
  48. final filesCount = sharedFiles != null
  49. ? sharedFiles!.length
  50. : selectedFiles?.files.length ?? 0;
  51. if (collectionsWithThumbnail.isEmpty) {
  52. if (shouldShowCreateAlbum) {
  53. return _getNewAlbumWidget(context, filesCount);
  54. }
  55. return const EmptyState();
  56. }
  57. return ListView.separated(
  58. itemBuilder: (context, index) {
  59. if (index == 0 && shouldShowCreateAlbum) {
  60. return _getNewAlbumWidget(context, filesCount);
  61. }
  62. final item =
  63. collectionsWithThumbnail[index - (shouldShowCreateAlbum ? 1 : 0)];
  64. return GestureDetector(
  65. behavior: HitTestBehavior.opaque,
  66. onTap: () => _albumListItemOnTap(context, item),
  67. child: AlbumListItemWidget(
  68. item,
  69. ),
  70. );
  71. },
  72. separatorBuilder: (context, index) => const SizedBox(
  73. height: 8,
  74. ),
  75. itemCount:
  76. collectionsWithThumbnail.length + (shouldShowCreateAlbum ? 1 : 0),
  77. shrinkWrap: true,
  78. physics: const BouncingScrollPhysics(),
  79. );
  80. }
  81. GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) {
  82. return GestureDetector(
  83. onTap: () async {
  84. await _createNewAlbumOnTap(context, filesCount);
  85. },
  86. behavior: HitTestBehavior.opaque,
  87. child: const NewAlbumListItemWidget(),
  88. );
  89. }
  90. Future<void> _createNewAlbumOnTap(
  91. BuildContext context,
  92. int filesCount,
  93. ) async {
  94. if (filesCount > 0) {
  95. final result = await showTextInputDialog(
  96. context,
  97. title: S.of(context).albumTitle,
  98. submitButtonLabel: S.of(context).ok,
  99. hintText: S.of(context).enterAlbumName,
  100. onSubmit: (name) {
  101. return _nameAlbum(context, name);
  102. },
  103. showOnlyLoadingState: true,
  104. textCapitalization: TextCapitalization.words,
  105. );
  106. if (result is Exception) {
  107. showGenericErrorDialog(
  108. context: context,
  109. );
  110. _logger.severe(
  111. "Failed to name album",
  112. result,
  113. );
  114. }
  115. } else {
  116. Navigator.pop(context);
  117. await showToast(
  118. context,
  119. S.of(context).createAlbumActionHint,
  120. toastLength: Toast.LENGTH_LONG,
  121. );
  122. Bus.instance.fire(
  123. TabChangedEvent(
  124. 0,
  125. TabChangedEventSource.collectionsPage,
  126. ),
  127. );
  128. }
  129. }
  130. Future<void> _nameAlbum(BuildContext context, String albumName) async {
  131. if (albumName.isNotEmpty) {
  132. final collection = await _createAlbum(albumName);
  133. if (collection != null) {
  134. if (await _runCollectionAction(
  135. context,
  136. collection,
  137. showProgressDialog: false,
  138. )) {
  139. if (actionType == CollectionActionType.restoreFiles) {
  140. showShortToast(
  141. context,
  142. 'Restored files to album ' + albumName,
  143. );
  144. } else {
  145. showShortToast(
  146. context,
  147. "Album '" + albumName + "' created.",
  148. );
  149. }
  150. _navigateToCollection(context, collection);
  151. }
  152. }
  153. }
  154. }
  155. Future<Collection?> _createAlbum(String albumName) async {
  156. Collection? collection;
  157. try {
  158. collection = await CollectionsService.instance.createAlbum(albumName);
  159. } catch (e, s) {
  160. _logger.severe("Failed to create album", e, s);
  161. rethrow;
  162. }
  163. return collection;
  164. }
  165. Future<void> _albumListItemOnTap(
  166. BuildContext context,
  167. CollectionWithThumbnail item,
  168. ) async {
  169. if (await _runCollectionAction(context, item.collection)) {
  170. late final String toastMessage;
  171. bool shouldNavigateToCollection = false;
  172. if (actionType == CollectionActionType.addFiles) {
  173. toastMessage =
  174. S.of(context).addedSuccessfullyTo(item.collection.displayName);
  175. shouldNavigateToCollection = true;
  176. } else if (actionType == CollectionActionType.moveFiles ||
  177. actionType == CollectionActionType.restoreFiles ||
  178. actionType == CollectionActionType.unHide) {
  179. toastMessage =
  180. S.of(context).movedSuccessfullyTo(item.collection.displayName);
  181. shouldNavigateToCollection = true;
  182. } else {
  183. toastMessage = "";
  184. }
  185. if (toastMessage.isNotEmpty) {
  186. showShortToast(
  187. context,
  188. toastMessage,
  189. );
  190. }
  191. if (shouldNavigateToCollection) {
  192. _navigateToCollection(
  193. context,
  194. item.collection,
  195. );
  196. }
  197. }
  198. }
  199. Future<bool> _runCollectionAction(
  200. BuildContext context,
  201. Collection collection, {
  202. bool showProgressDialog = true,
  203. }) async {
  204. switch (actionType) {
  205. case CollectionActionType.addFiles:
  206. return _addToCollection(
  207. context,
  208. collection.id,
  209. showProgressDialog,
  210. );
  211. case CollectionActionType.moveFiles:
  212. return _moveFilesToCollection(context, collection.id);
  213. case CollectionActionType.unHide:
  214. return _moveFilesToCollection(context, collection.id);
  215. case CollectionActionType.restoreFiles:
  216. return _restoreFilesToCollection(context, collection.id);
  217. case CollectionActionType.shareCollection:
  218. return _showShareCollectionPage(context, collection);
  219. case CollectionActionType.collectPhotos:
  220. return _createCollaborativeLink(context, collection);
  221. }
  222. }
  223. void _navigateToCollection(BuildContext context, Collection collection) {
  224. Navigator.pop(context);
  225. routeToPage(
  226. context,
  227. CollectionPage(
  228. CollectionWithThumbnail(collection, null),
  229. ),
  230. );
  231. }
  232. Future<bool> _createCollaborativeLink(
  233. BuildContext context,
  234. Collection collection,
  235. ) async {
  236. final CollectionActions collectionActions =
  237. CollectionActions(CollectionsService.instance);
  238. if (collection.hasLink) {
  239. if (collection.publicURLs!.first!.enableCollect) {
  240. if (Configuration.instance.getUserID() == collection.owner!.id) {
  241. unawaited(
  242. routeToPage(
  243. context,
  244. ShareCollectionPage(collection),
  245. ),
  246. );
  247. }
  248. showShortToast(
  249. context,
  250. S.of(context).thisAlbumAlreadyHDACollaborativeLink,
  251. );
  252. return Future.value(false);
  253. } else {
  254. try {
  255. unawaited(
  256. routeToPage(
  257. context,
  258. ShareCollectionPage(collection),
  259. ),
  260. );
  261. CollectionsService.instance
  262. .updateShareUrl(collection, {'enableCollect': true}).then(
  263. (value) => showShortToast(
  264. context,
  265. S.of(context).collaborativeLinkCreatedFor(collection.displayName),
  266. ),
  267. );
  268. return true;
  269. } catch (e) {
  270. showGenericErrorDialog(context: context);
  271. return false;
  272. }
  273. }
  274. }
  275. final bool result = await collectionActions.enableUrl(
  276. context,
  277. collection,
  278. enableCollect: true,
  279. );
  280. if (result) {
  281. showShortToast(
  282. context,
  283. S.of(context).collaborativeLinkCreatedFor(collection.displayName),
  284. );
  285. if (Configuration.instance.getUserID() == collection.owner!.id) {
  286. unawaited(
  287. routeToPage(
  288. context,
  289. ShareCollectionPage(collection),
  290. ),
  291. );
  292. } else {
  293. showGenericErrorDialog(context: context);
  294. _logger.severe("Cannot share collections owned by others");
  295. }
  296. }
  297. return result;
  298. }
  299. Future<bool> _showShareCollectionPage(
  300. BuildContext context,
  301. Collection collection,
  302. ) {
  303. if (Configuration.instance.getUserID() == collection.owner!.id) {
  304. unawaited(
  305. routeToPage(
  306. context,
  307. ShareCollectionPage(collection),
  308. ),
  309. );
  310. } else {
  311. showGenericErrorDialog(context: context);
  312. _logger.severe("Cannot share collections owned by others");
  313. }
  314. return Future.value(true);
  315. }
  316. Future<bool> _addToCollection(
  317. BuildContext context,
  318. int collectionID,
  319. bool showProgressDialog,
  320. ) async {
  321. final dialog = showProgressDialog
  322. ? createProgressDialog(
  323. context,
  324. S.of(context).uploadingFilesToAlbum,
  325. isDismissible: true,
  326. )
  327. : null;
  328. await dialog?.show();
  329. try {
  330. final List<File> files = [];
  331. final List<File> filesPendingUpload = [];
  332. final int currentUserID = Configuration.instance.getUserID()!;
  333. if (sharedFiles != null) {
  334. filesPendingUpload.addAll(
  335. await convertIncomingSharedMediaToFile(
  336. sharedFiles!,
  337. collectionID,
  338. ),
  339. );
  340. } else {
  341. for (final file in selectedFiles!.files) {
  342. File? currentFile;
  343. if (file.uploadedFileID != null) {
  344. currentFile = file;
  345. } else if (file.generatedID != null) {
  346. // when file is not uploaded, refresh the state from the db to
  347. // ensure we have latest upload status for given file before
  348. // queueing it up as pending upload
  349. currentFile = await (FilesDB.instance.getFile(file.generatedID!));
  350. } else if (file.generatedID == null) {
  351. _logger.severe("generated id should not be null");
  352. }
  353. if (currentFile == null) {
  354. _logger.severe("Failed to find fileBy genID");
  355. continue;
  356. }
  357. if (currentFile.uploadedFileID == null) {
  358. currentFile.collectionID = collectionID;
  359. filesPendingUpload.add(currentFile);
  360. } else {
  361. files.add(currentFile);
  362. }
  363. }
  364. }
  365. if (filesPendingUpload.isNotEmpty) {
  366. // Newly created collection might not be cached
  367. final Collection? c =
  368. CollectionsService.instance.getCollectionByID(collectionID);
  369. if (c != null && c.owner!.id != currentUserID) {
  370. showToast(context, S.of(context).canNotUploadToAlbumsOwnedByOthers);
  371. await dialog?.hide();
  372. return false;
  373. } else {
  374. // filesPendingUpload might be getting ignored during auto-upload
  375. // because the user deleted these files from ente in the past.
  376. await IgnoredFilesService.instance
  377. .removeIgnoredMappings(filesPendingUpload);
  378. await FilesDB.instance.insertMultiple(filesPendingUpload);
  379. }
  380. }
  381. if (files.isNotEmpty) {
  382. await CollectionsService.instance.addToCollection(collectionID, files);
  383. }
  384. RemoteSyncService.instance.sync(silently: true);
  385. await dialog?.hide();
  386. selectedFiles?.clearAll();
  387. return true;
  388. } catch (e, s) {
  389. _logger.severe("Failed to add to album", e, s);
  390. await dialog?.hide();
  391. showGenericErrorDialog(context: context);
  392. rethrow;
  393. }
  394. }
  395. Future<bool> _moveFilesToCollection(
  396. BuildContext context,
  397. int toCollectionID,
  398. ) async {
  399. final String message = actionType == CollectionActionType.moveFiles
  400. ? S.of(context).movingFilesToAlbum
  401. : S.of(context).unhidingFilesToAlbum;
  402. final dialog = createProgressDialog(context, message, isDismissible: true);
  403. await dialog.show();
  404. try {
  405. final int fromCollectionID = selectedFiles!.files.first.collectionID!;
  406. await CollectionsService.instance.move(
  407. toCollectionID,
  408. fromCollectionID,
  409. selectedFiles!.files.toList(),
  410. );
  411. await dialog.hide();
  412. RemoteSyncService.instance.sync(silently: true);
  413. selectedFiles?.clearAll();
  414. return true;
  415. } on AssertionError catch (e) {
  416. await dialog.hide();
  417. showErrorDialog(context, S.of(context).oops, e.message as String?);
  418. return false;
  419. } catch (e, s) {
  420. _logger.severe("Could not move to album", e, s);
  421. await dialog.hide();
  422. showGenericErrorDialog(context: context);
  423. return false;
  424. }
  425. }
  426. Future<bool> _restoreFilesToCollection(
  427. BuildContext context,
  428. int toCollectionID,
  429. ) async {
  430. final dialog = createProgressDialog(
  431. context,
  432. S.of(context).restoringFiles,
  433. isDismissible: true,
  434. );
  435. await dialog.show();
  436. try {
  437. await CollectionsService.instance
  438. .restore(toCollectionID, selectedFiles!.files.toList());
  439. RemoteSyncService.instance.sync(silently: true);
  440. selectedFiles?.clearAll();
  441. await dialog.hide();
  442. return true;
  443. } on AssertionError catch (e) {
  444. await dialog.hide();
  445. showErrorDialog(context, S.of(context).oops, e.message as String?);
  446. return false;
  447. } catch (e, s) {
  448. _logger.severe("Could not move to album", e, s);
  449. await dialog.hide();
  450. showGenericErrorDialog(context: context);
  451. return false;
  452. }
  453. }
  454. }