collections_gallery_widget.dart 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import 'dart:async';
  2. import 'package:collection/collection.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/core/constants.dart';
  7. import 'package:photos/core/event_bus.dart';
  8. import 'package:photos/events/collection_updated_event.dart';
  9. import 'package:photos/events/local_photos_updated_event.dart';
  10. import 'package:photos/events/user_logged_out_event.dart';
  11. import 'package:photos/extensions/list.dart';
  12. import 'package:photos/models/collection.dart';
  13. import 'package:photos/models/collection_items.dart';
  14. import 'package:photos/services/collections_service.dart';
  15. import 'package:photos/ui/collections/archived_collections_button_widget.dart';
  16. import 'package:photos/ui/collections/device_folders_grid_view_widget.dart';
  17. import 'package:photos/ui/collections/hidden_collections_button_widget.dart';
  18. import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
  19. import 'package:photos/ui/collections/section_title.dart';
  20. import 'package:photos/ui/collections/trash_button_widget.dart';
  21. import 'package:photos/ui/collections/uncat_collections_button_widget.dart';
  22. import 'package:photos/ui/common/loading_widget.dart';
  23. import 'package:photos/ui/viewer/actions/delete_empty_albums.dart';
  24. import 'package:photos/ui/viewer/gallery/empty_state.dart';
  25. import 'package:photos/utils/local_settings.dart';
  26. class CollectionsGalleryWidget extends StatefulWidget {
  27. const CollectionsGalleryWidget({Key? key}) : super(key: key);
  28. @override
  29. State<CollectionsGalleryWidget> createState() =>
  30. _CollectionsGalleryWidgetState();
  31. }
  32. class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
  33. with AutomaticKeepAliveClientMixin {
  34. final _logger = Logger((_CollectionsGalleryWidgetState).toString());
  35. late StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
  36. late StreamSubscription<CollectionUpdatedEvent>
  37. _collectionUpdatesSubscription;
  38. late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
  39. AlbumSortKey? sortKey;
  40. String _loadReason = "init";
  41. @override
  42. void initState() {
  43. _localFilesSubscription =
  44. Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
  45. _loadReason = event.reason;
  46. setState(() {});
  47. });
  48. _collectionUpdatesSubscription =
  49. Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
  50. _loadReason = event.reason;
  51. setState(() {});
  52. });
  53. _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
  54. _loadReason = event.reason;
  55. setState(() {});
  56. });
  57. sortKey = LocalSettings.instance.albumSortKey();
  58. super.initState();
  59. }
  60. @override
  61. Widget build(BuildContext context) {
  62. super.build(context);
  63. _logger.info("Building, trigger: $_loadReason");
  64. return FutureBuilder<List<CollectionWithThumbnail>>(
  65. future: _getCollections(),
  66. builder: (context, snapshot) {
  67. if (snapshot.hasData) {
  68. return _getCollectionsGalleryWidget(snapshot.data);
  69. } else if (snapshot.hasError) {
  70. return Text(snapshot.error.toString());
  71. } else {
  72. return const EnteLoadingWidget();
  73. }
  74. },
  75. );
  76. }
  77. Future<List<CollectionWithThumbnail>> _getCollections() async {
  78. final List<CollectionWithThumbnail> collectionsWithThumbnail =
  79. await CollectionsService.instance.getCollectionsWithThumbnails();
  80. // Remove uncategorized collection
  81. collectionsWithThumbnail
  82. .removeWhere((t) => t.collection.type == CollectionType.uncategorized);
  83. final ListMatch<CollectionWithThumbnail> favMathResult =
  84. collectionsWithThumbnail.splitMatch(
  85. (element) => element.collection.type == CollectionType.favorites,
  86. );
  87. // Hide fav collection if it's empty and not shared
  88. favMathResult.matched.removeWhere(
  89. (element) =>
  90. element.thumbnail == null &&
  91. (element.collection.publicURLs?.isEmpty ?? false),
  92. );
  93. favMathResult.unmatched.sort(
  94. (first, second) {
  95. if (sortKey == AlbumSortKey.albumName) {
  96. return compareAsciiLowerCaseNatural(
  97. first.collection.name!,
  98. second.collection.name!,
  99. );
  100. } else if (sortKey == AlbumSortKey.newestPhoto) {
  101. return (second.thumbnail?.creationTime ?? -1 * intMaxValue)
  102. .compareTo(first.thumbnail?.creationTime ?? -1 * intMaxValue);
  103. } else {
  104. return second.collection.updationTime
  105. .compareTo(first.collection.updationTime);
  106. }
  107. },
  108. );
  109. // This is a way to identify collection which were automatically created
  110. // during create link flow for selected files
  111. final ListMatch<CollectionWithThumbnail> potentialSharedLinkCollection =
  112. favMathResult.unmatched.splitMatch(
  113. (e) => (e.collection.isSharedFilesCollection()),
  114. );
  115. return favMathResult.matched + potentialSharedLinkCollection.unmatched;
  116. }
  117. Widget _getCollectionsGalleryWidget(
  118. List<CollectionWithThumbnail>? collections,
  119. ) {
  120. final bool showDeleteAlbumsButton =
  121. collections!.where((c) => c.thumbnail == null).length >= 3;
  122. final TextStyle trashAndHiddenTextStyle = Theme.of(context)
  123. .textTheme
  124. .subtitle1!
  125. .copyWith(
  126. color: Theme.of(context).textTheme.subtitle1!.color!.withOpacity(0.5),
  127. );
  128. return SingleChildScrollView(
  129. child: Container(
  130. margin: const EdgeInsets.only(bottom: 50),
  131. child: Column(
  132. children: [
  133. const SizedBox(height: 12),
  134. const SectionTitle(title: "On device"),
  135. const SizedBox(height: 12),
  136. const DeviceFoldersGridViewWidget(),
  137. const Padding(padding: EdgeInsets.all(4)),
  138. const Divider(),
  139. Row(
  140. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  141. crossAxisAlignment: CrossAxisAlignment.end,
  142. children: [
  143. SectionTitle(titleWithBrand: getOnEnteSection(context)),
  144. _sortMenu(),
  145. ],
  146. ),
  147. showDeleteAlbumsButton
  148. ? const Padding(
  149. padding: EdgeInsets.only(top: 2, left: 8.5, right: 48),
  150. child: DeleteEmptyAlbums(),
  151. )
  152. : const SizedBox.shrink(),
  153. const SizedBox(height: 12),
  154. Configuration.instance.hasConfiguredAccount()
  155. ? RemoteCollectionsGridViewWidget(collections)
  156. : const EmptyState(),
  157. const SizedBox(height: 10),
  158. const Divider(),
  159. const SizedBox(height: 16),
  160. Padding(
  161. padding: const EdgeInsets.symmetric(horizontal: 16),
  162. child: Column(
  163. children: [
  164. UnCatCollectionsButtonWidget(trashAndHiddenTextStyle),
  165. const SizedBox(height: 12),
  166. ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
  167. const SizedBox(height: 12),
  168. HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
  169. const SizedBox(height: 12),
  170. TrashButtonWidget(trashAndHiddenTextStyle),
  171. ],
  172. ),
  173. ),
  174. const Padding(padding: EdgeInsets.fromLTRB(12, 12, 12, 36)),
  175. ],
  176. ),
  177. ),
  178. );
  179. }
  180. Widget _sortMenu() {
  181. Text sortOptionText(AlbumSortKey key) {
  182. String text = key.toString();
  183. switch (key) {
  184. case AlbumSortKey.albumName:
  185. text = "Name";
  186. break;
  187. case AlbumSortKey.newestPhoto:
  188. text = "Newest";
  189. break;
  190. case AlbumSortKey.lastUpdated:
  191. text = "Last updated";
  192. }
  193. return Text(
  194. text,
  195. style: Theme.of(context).textTheme.subtitle1!.copyWith(
  196. fontSize: 14,
  197. color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
  198. ),
  199. );
  200. }
  201. return Padding(
  202. padding: const EdgeInsets.only(right: 24),
  203. child: PopupMenuButton(
  204. offset: const Offset(10, 50),
  205. initialValue: sortKey?.index ?? 0,
  206. child: Align(
  207. child: Row(
  208. mainAxisAlignment: MainAxisAlignment.end,
  209. crossAxisAlignment: CrossAxisAlignment.center,
  210. children: [
  211. const Padding(
  212. padding: EdgeInsets.only(left: 5.0),
  213. ),
  214. Container(
  215. width: 36,
  216. height: 36,
  217. decoration: BoxDecoration(
  218. color: Theme.of(context).hintColor.withOpacity(0.2),
  219. borderRadius: BorderRadius.circular(18),
  220. ),
  221. child: Icon(
  222. Icons.sort,
  223. color: Theme.of(context).iconTheme.color,
  224. size: 20,
  225. ),
  226. ),
  227. ],
  228. ),
  229. ),
  230. onSelected: (int index) async {
  231. sortKey = AlbumSortKey.values[index];
  232. await LocalSettings.instance.setAlbumSortKey(sortKey!);
  233. setState(() {});
  234. },
  235. itemBuilder: (context) {
  236. return List.generate(AlbumSortKey.values.length, (index) {
  237. return PopupMenuItem(
  238. value: index,
  239. child: sortOptionText(AlbumSortKey.values[index]),
  240. );
  241. });
  242. },
  243. ),
  244. );
  245. }
  246. @override
  247. void dispose() {
  248. _localFilesSubscription.cancel();
  249. _collectionUpdatesSubscription.cancel();
  250. _loggedOutEvent.cancel();
  251. super.dispose();
  252. }
  253. @override
  254. bool get wantKeepAlive => true;
  255. }