import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:photos/core/configuration.dart"; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection.dart'; import 'package:photos/services/collections_service.dart'; import "package:photos/services/favorites_service.dart"; import "package:photos/ui/collections/button/archived_button.dart"; import "package:photos/ui/collections/button/hidden_button.dart"; import "package:photos/ui/collections/button/trash_button.dart"; import "package:photos/ui/collections/button/uncategorized_button.dart"; import "package:photos/ui/collections/collection_list_page.dart"; import "package:photos/ui/collections/create_new_album_widget.dart"; import "package:photos/ui/collections/device/device_folders_grid_view.dart"; import "package:photos/ui/collections/device/device_folders_vertical_grid_view.dart"; import "package:photos/ui/collections/flex_grid_view.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/tabs/section_title.dart"; import "package:photos/ui/viewer/actions/delete_empty_albums.dart"; import "package:photos/ui/viewer/gallery/empty_state.dart"; import 'package:photos/utils/local_settings.dart'; import "package:photos/utils/navigation_util.dart"; class UserCollectionsTab extends StatefulWidget { const UserCollectionsTab({Key? key}) : super(key: key); @override State createState() => _UserCollectionsTabState(); } class _UserCollectionsTabState extends State with AutomaticKeepAliveClientMixin { final _logger = Logger((_UserCollectionsTabState).toString()); late StreamSubscription _localFilesSubscription; late StreamSubscription _collectionUpdatesSubscription; late StreamSubscription _loggedOutEvent; AlbumSortKey? sortKey; String _loadReason = "init"; final _scrollController = ScrollController(); static const int _kOnEnteItemLimitCount = 10; @override void initState() { _localFilesSubscription = Bus.instance.on().listen((event) { _loadReason = event.reason; setState(() {}); }); _collectionUpdatesSubscription = Bus.instance.on().listen((event) { _loadReason = event.reason; setState(() {}); }); _loggedOutEvent = Bus.instance.on().listen((event) { _loadReason = event.reason; setState(() {}); }); sortKey = LocalSettings.instance.albumSortKey(); super.initState(); } @override Widget build(BuildContext context) { super.build(context); _logger.info("Building, trigger: $_loadReason"); return FutureBuilder>( future: _getCollections(), builder: (context, snapshot) { if (snapshot.hasData) { return _getCollectionsGalleryWidget(snapshot.data!); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } else { return const EnteLoadingWidget(); } }, ); } Future> _getCollections() async { final List collections = CollectionsService.instance.getCollectionsForUI(); final bool hasFavorites = FavoritesService.instance.hasFavorites(); late Map collectionIDToNewestPhotoTime; if (sortKey == AlbumSortKey.newestPhoto) { collectionIDToNewestPhotoTime = await CollectionsService.instance.getCollectionIDToNewestFileTime(); } collections.sort( (first, second) { if (sortKey == AlbumSortKey.albumName) { return compareAsciiLowerCaseNatural( first.displayName, second.displayName, ); } else if (sortKey == AlbumSortKey.newestPhoto) { return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) .compareTo( collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, ); } else { return second.updationTime.compareTo(first.updationTime); } }, ); final List favorites = []; final List pinned = []; final List rest = []; for (final collection in collections) { if (collection.type == CollectionType.uncategorized || collection.isSharedFilesCollection() || collection.isHidden()) { continue; } if (collection.type == CollectionType.favorites) { // Hide fav collection if it's empty if (hasFavorites) { favorites.add(collection); } } else if (collection.isPinned) { pinned.add(collection); } else { rest.add(collection); } } return favorites + pinned + rest; } Widget _getCollectionsGalleryWidget(List collections) { final TextStyle trashAndHiddenTextStyle = Theme.of(context).textTheme.titleMedium!.copyWith( color: Theme.of(context) .textTheme .titleMedium! .color! .withOpacity(0.5), ); return CustomScrollView( controller: _scrollController, slivers: [ SliverToBoxAdapter( child: Hero( tag: "OnDeviceAppTitle", child: SectionOptions( SectionTitle(title: S.of(context).onDevice), trailingWidget: IconButtonWidget( icon: Icons.chevron_right, iconButtonType: IconButtonType.secondary, onTap: () { unawaited( routeToPage( context, DeviceFolderVerticalGridView( appTitle: SectionTitle( title: S.of(context).onDevice, ), tag: "OnDeviceAppTitle", ), ), ); }, ), ), ), ), const SliverToBoxAdapter(child: DeviceFoldersGridView()), SliverToBoxAdapter( child: SectionOptions( SectionTitle(titleWithBrand: getOnEnteSection(context)), trailingWidget: _sortMenu(collections), padding: const EdgeInsets.only(left: 12, right: 6), ), ), SliverToBoxAdapter(child: DeleteEmptyAlbums(collections ?? [])), Configuration.instance.hasConfiguredAccount() ? CollectionsFlexiGridViewWidget( collections, displayLimitCount: _kOnEnteItemLimitCount, shrinkWrap: true, ) : const SliverToBoxAdapter(child: EmptyState()), collections.length > _kOnEnteItemLimitCount ? SliverToBoxAdapter( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { unawaited( routeToPage( context, CollectionListPage( collections, appTitle: SectionTitle( titleWithBrand: getOnEnteSection(context), ), initalScrollOffset: _scrollController.offset, ), ), ); }, child: SectionOptions( SectionTitle( title: S.of(context).viewAll, mutedTitle: true, ), trailingWidget: const IconButtonWidget( icon: Icons.chevron_right, iconButtonType: IconButtonType.secondary, ), ), ), ) : const SliverToBoxAdapter(child: SizedBox.shrink()), const SliverToBoxAdapter(child: Divider()), const SliverToBoxAdapter(child: SizedBox(height: 12)), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Column( children: [ UnCategorizedCollections(trashAndHiddenTextStyle), const SizedBox(height: 12), ArchivedCollectionsButton(trashAndHiddenTextStyle), const SizedBox(height: 12), HiddenCollectionsButtonWidget(trashAndHiddenTextStyle), const SizedBox(height: 12), TrashSectionButton(trashAndHiddenTextStyle), ], ), ), ), const SliverToBoxAdapter(child: SizedBox(height: 48)), ], ); } Widget _sortMenu(List collections) { Text sortOptionText(AlbumSortKey key) { String text = key.toString(); switch (key) { case AlbumSortKey.albumName: text = S.of(context).name; break; case AlbumSortKey.newestPhoto: text = S.of(context).newest; break; case AlbumSortKey.lastUpdated: text = S.of(context).lastUpdated; } return Text( text, style: Theme.of(context).textTheme.titleMedium!.copyWith( fontSize: 14, color: Theme.of(context).iconTheme.color!.withOpacity(0.7), ), ); } return Theme( data: Theme.of(context).copyWith( highlightColor: Colors.transparent, splashColor: Colors.transparent, ), child: Row( children: [ const CreateNewAlbumIcon(), GestureDetector( onTapDown: (TapDownDetails details) async { final int? selectedValue = await showMenu( context: context, position: RelativeRect.fromLTRB( details.globalPosition.dx, details.globalPosition.dy, details.globalPosition.dx, details.globalPosition.dy + 50, ), items: List.generate(AlbumSortKey.values.length, (index) { return PopupMenuItem( value: index, child: sortOptionText(AlbumSortKey.values[index]), ); }), ); if (selectedValue != null) { sortKey = AlbumSortKey.values[selectedValue]; ; await LocalSettings.instance.setAlbumSortKey(sortKey!); setState(() {}); } }, child: const IconButtonWidget( icon: Icons.sort_outlined, iconButtonType: IconButtonType.secondary, ), ), ], ), ); } @override void dispose() { _localFilesSubscription.cancel(); _collectionUpdatesSubscription.cancel(); _loggedOutEvent.cancel(); _scrollController.dispose(); super.dispose(); } @override bool get wantKeepAlive => true; }