diff --git a/mobile/lib/ui/viewer/gallery/collection_page.dart b/mobile/lib/ui/viewer/gallery/collection_page.dart index 57990b0b1e..ed56c2f28f 100644 --- a/mobile/lib/ui/viewer/gallery/collection_page.dart +++ b/mobile/lib/ui/viewer/gallery/collection_page.dart @@ -19,6 +19,7 @@ import "package:photos/ui/viewer/gallery/empty_album_state.dart"; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; +import "package:photos/ui/viewer/gallery/hierarchical_search_gallery.dart"; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; @@ -114,7 +115,7 @@ class CollectionPage extends StatelessWidget { ), child: Scaffold( appBar: PreferredSize( - preferredSize: const Size.fromHeight(50.0), + preferredSize: const Size.fromHeight(90.0), child: GalleryAppBarWidget( galleryType, c.collection.displayName, @@ -134,7 +135,23 @@ class CollectionPage extends StatelessWidget { child: Stack( alignment: Alignment.bottomCenter, children: [ - gallery, + Builder( + builder: (context) { + return ValueListenableBuilder( + valueListenable: InheritedSearchFilterData.of(context) + .searchFilterDataProvider! + .isSearchingNotifier, + builder: (context, value, _) { + return value + ? HierarchicalSearchGallery( + tagPrefix: tagPrefix, + selectedFiles: _selectedFiles, + ) + : gallery; + }, + ); + }, + ), FileSelectionOverlayBar( galleryType, _selectedFiles, diff --git a/mobile/lib/ui/viewer/gallery/gallery.dart b/mobile/lib/ui/viewer/gallery/gallery.dart index 296fe7d31a..df5aa8745f 100644 --- a/mobile/lib/ui/viewer/gallery/gallery.dart +++ b/mobile/lib/ui/viewer/gallery/gallery.dart @@ -18,7 +18,6 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; -import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; import "package:photos/utils/debouncer.dart"; import "package:photos/utils/hierarchical_search_util.dart"; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -114,7 +113,6 @@ class GalleryState extends State { late String _logTag; bool _sortOrderAsc = false; List _allGalleryFiles = []; - late SearchFilterDataProvider? _searchFilterDataProvider; @override void initState() { @@ -185,49 +183,6 @@ class GalleryState extends State { }); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _searchFilterDataProvider = - InheritedSearchFilterData.maybeOf(context)?.searchFilterDataProvider; - - if (_searchFilterDataProvider != null) { - _searchFilterDataProvider! - .removeListener(fromApplied: true, listener: _onFiltersUpdated); - _searchFilterDataProvider! - .addListener(toApplied: true, listener: _onFiltersUpdated); - } - } - - void _onFiltersUpdated() async { - final filters = _searchFilterDataProvider!.appliedFilters; - if (filters.isEmpty) { - Navigator.of(context).pop(); - return; - } - - final filterdFiles = await getFilteredFiles(filters); - _setFilteredFilesAndReload(filterdFiles); - curateAlbumFilters(_searchFilterDataProvider!, filterdFiles); - } - - void _setFilteredFilesAndReload(List files) { - final updatedGroupedFiles = - widget.enableFileGrouping && widget.groupType.timeGrouping() - ? _groupBasedOnTime(files) - : _genericGroupForPerf(files); - - _allGalleryFiles = [ - for (List group in updatedGroupedFiles) ...group, - ]; - - if (mounted) { - setState(() { - currentGroupedFiles = updatedGroupedFiles; - }); - } - } - void _setFilesAndReload(List files) { final hasReloaded = _onFilesLoaded(files); if (!hasReloaded && mounted) { @@ -279,11 +234,13 @@ class GalleryState extends State { "ms", ); + /// To curate filters when a gallery is first opened. if (!result.hasMore) { final searchFilterDataProvider = InheritedSearchFilterData.maybeOf(context) ?.searchFilterDataProvider; - if (searchFilterDataProvider != null) { + if (searchFilterDataProvider != null && + !searchFilterDataProvider.isSearchingNotifier.value) { curateAlbumFilters(searchFilterDataProvider, result.files); } } @@ -303,10 +260,6 @@ class GalleryState extends State { subscription.cancel(); } _debouncer.cancelDebounceTimer(); - if (_searchFilterDataProvider != null) { - _searchFilterDataProvider! - .removeListener(fromApplied: true, listener: _onFiltersUpdated); - } super.dispose(); } diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index eb7d3a16ba..7a458eb872 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -148,14 +148,14 @@ class _GalleryAppBarWidgetState extends State { ), ), const SizedBox( - width: 300, + width: 200, height: 50, child: AppliedFilters(), ), ], ), ), - actions: _getDefaultActions(context), + // actions: _getDefaultActions(context), bottom: galleryType == GalleryType.searchResults || galleryType == GalleryType.ownedCollection ? const PreferredSize( diff --git a/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart b/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart new file mode 100644 index 0000000000..8bef1f4b36 --- /dev/null +++ b/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart @@ -0,0 +1,131 @@ +import "dart:async"; +import "dart:developer"; + +import "package:flutter/material.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/files_updated_event.dart"; +import "package:photos/events/local_photos_updated_event.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/file_load_result.dart"; +import "package:photos/models/selected_files.dart"; +import "package:photos/ui/viewer/gallery/gallery.dart"; +import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; +import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; +import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; +import "package:photos/utils/hierarchical_search_util.dart"; + +class HierarchicalSearchGallery extends StatefulWidget { + final String tagPrefix; + final SelectedFiles? selectedFiles; + const HierarchicalSearchGallery({ + required this.tagPrefix, + this.selectedFiles, + super.key, + }); + + @override + State createState() => + _HierarchicalSearchGalleryState(); +} + +class _HierarchicalSearchGalleryState extends State { + StreamSubscription? _filesUpdatedEvent; + late SearchFilterDataProvider? _searchFilterDataProvider; + List _filterdFiles = []; + int _filteredFilesVersion = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + if (_filesUpdatedEvent != null) { + _filesUpdatedEvent!.cancel(); + } + _filesUpdatedEvent = + Bus.instance.on().listen((event) { + if (event.type == EventType.deletedFromDevice || + event.type == EventType.deletedFromEverywhere || + event.type == EventType.deletedFromRemote || + event.type == EventType.hide) { + for (var updatedFile in event.updatedFiles) { + _filterdFiles.remove(updatedFile); + GalleryFilesState.of(context).galleryFiles.remove(updatedFile); + } + setState(() {}); + } + }); + + _searchFilterDataProvider = InheritedSearchFilterData.maybeOf(context) + ?.searchFilterDataProvider; + + if (_searchFilterDataProvider != null) { + _searchFilterDataProvider! + .removeListener(fromApplied: true, listener: _onFiltersUpdated); + _searchFilterDataProvider! + .addListener(toApplied: true, listener: _onFiltersUpdated); + } + _onFiltersUpdated(); + } catch (e) { + log('An error occurred: $e'); + } + }); + } + + void _onFiltersUpdated() async { + final filters = _searchFilterDataProvider!.appliedFilters; + if (filters.isEmpty) { + Navigator.of(context).pop(); + return; + } + + final filterdFiles = await getFilteredFiles(filters); + _setFilteredFilesAndReload(filterdFiles); + curateAlbumFilters(_searchFilterDataProvider!, filterdFiles); + } + + void _setFilteredFilesAndReload(List files) { + if (mounted) { + setState(() { + _filterdFiles = files; + GalleryFilesState.of(context).setGalleryFiles = files; + _filteredFilesVersion++; + }); + } + } + + @override + void dispose() { + _filesUpdatedEvent?.cancel(); + if (_searchFilterDataProvider != null) { + _searchFilterDataProvider! + .removeListener(fromApplied: true, listener: _onFiltersUpdated); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Gallery( + key: ValueKey(_filteredFilesVersion), + asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async { + final files = _filterdFiles + .where( + (file) => + file.creationTime! >= creationStartTime && + file.creationTime! <= creationEndTime, + ) + .toList(); + return FileLoadResult(files, false); + }, + tagPrefix: widget.tagPrefix, + reloadEvent: Bus.instance.on(), + removalEventTypes: const { + EventType.deletedFromRemote, + EventType.deletedFromEverywhere, + EventType.hide, + }, + selectedFiles: widget.selectedFiles, + ); + } +} diff --git a/mobile/lib/ui/viewer/gallery/state/gallery_files_inherited_widget.dart b/mobile/lib/ui/viewer/gallery/state/gallery_files_inherited_widget.dart index 27ae8943b7..bd07d93309 100644 --- a/mobile/lib/ui/viewer/gallery/state/gallery_files_inherited_widget.dart +++ b/mobile/lib/ui/viewer/gallery/state/gallery_files_inherited_widget.dart @@ -18,6 +18,10 @@ class GalleryFilesState extends InheritedWidget { _galleryFiles = galleryFiles; } + void removeFile(EnteFile file) { + _galleryFiles!.remove(file); + } + List get galleryFiles { if (_galleryFiles == null) { throw Exception( diff --git a/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart b/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart index c18e31cb6a..a865419818 100644 --- a/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart +++ b/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart @@ -8,7 +8,7 @@ class SearchFilterDataProvider { //TODO: Make this non-nullable and required so every time this is wrapped //over a gallery's scaffold, it's forced to provide an initial gallery filter HierarchicalSearchFilter? initialGalleryFilter; - bool _isSearching = false; + final isSearchingNotifier = ValueNotifier(false); List get recommendations => _recommendedFiltersNotifier.recommendedFilters; @@ -22,8 +22,8 @@ class SearchFilterDataProvider { void applyFilters(List filters) { _recommendedFiltersNotifier.removeFilters(filters); - if (!_isSearching) { - _isSearching = true; + if (!isSearchingNotifier.value) { + isSearchingNotifier.value = true; _appliedFiltersNotifier.addFilters([initialGalleryFilter!, ...filters]); } else { _appliedFiltersNotifier.addFilters(filters);