diff --git a/mobile/lib/events/magic_sort_change_event.dart b/mobile/lib/events/magic_sort_change_event.dart new file mode 100644 index 0000000000..846ebe4709 --- /dev/null +++ b/mobile/lib/events/magic_sort_change_event.dart @@ -0,0 +1,11 @@ +import "package:photos/events/event.dart"; + +enum MagicSortType { + mostRecent, + mostRelevant, +} + +class MagicSortChangeEvent extends Event { + final MagicSortType sortType; + MagicSortChangeEvent(this.sortType); +} diff --git a/mobile/lib/models/gallery_type.dart b/mobile/lib/models/gallery_type.dart index fb07f23a0a..4139d69bab 100644 --- a/mobile/lib/models/gallery_type.dart +++ b/mobile/lib/models/gallery_type.dart @@ -20,6 +20,7 @@ enum GalleryType { quickLink, peopleTag, cluster, + magic, } extension GalleyTypeExtension on GalleryType { @@ -36,6 +37,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.uncategorized: case GalleryType.peopleTag: case GalleryType.sharedCollection: + case GalleryType.magic: return true; case GalleryType.hiddenSection: @@ -65,6 +67,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.sharedCollection: case GalleryType.locationTag: case GalleryType.cluster: + case GalleryType.magic: return false; } } @@ -83,6 +86,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.quickLink: case GalleryType.peopleTag: case GalleryType.cluster: + case GalleryType.magic: return true; case GalleryType.trash: case GalleryType.archive: @@ -107,6 +111,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.locationTag: case GalleryType.quickLink: case GalleryType.peopleTag: + case GalleryType.magic: return true; case GalleryType.trash: case GalleryType.cluster: @@ -126,6 +131,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.locationTag: case GalleryType.peopleTag: case GalleryType.cluster: + case GalleryType.magic: return true; case GalleryType.hiddenSection: case GalleryType.hiddenOwnedCollection: @@ -155,6 +161,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.cluster: case GalleryType.trash: case GalleryType.locationTag: + case GalleryType.magic: return false; } } @@ -178,6 +185,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.sharedCollection: case GalleryType.locationTag: case GalleryType.cluster: + case GalleryType.magic: return false; } } @@ -195,6 +203,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.uncategorized: case GalleryType.locationTag: case GalleryType.quickLink: + case GalleryType.magic: return true; case GalleryType.hiddenSection: @@ -222,6 +231,7 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.uncategorized: case GalleryType.locationTag: case GalleryType.peopleTag: + case GalleryType.magic: return true; case GalleryType.hiddenSection: @@ -353,6 +363,7 @@ extension GalleryAppBarExtn on GalleryType { case GalleryType.localFolder: case GalleryType.locationTag: case GalleryType.searchResults: + case GalleryType.magic: return false; case GalleryType.cluster: case GalleryType.uncategorized: diff --git a/mobile/lib/services/magic_cache_service.dart b/mobile/lib/services/magic_cache_service.dart index b23247a3fe..f88f129558 100644 --- a/mobile/lib/services/magic_cache_service.dart +++ b/mobile/lib/services/magic_cache_service.dart @@ -11,6 +11,8 @@ import "package:photos/models/search/search_types.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/remote_assets_service.dart"; +import "package:photos/ui/viewer/search/result/magic_result_screen.dart"; +import "package:photos/utils/navigation_util.dart"; import "package:shared_preferences/shared_preferences.dart"; class MagicCache { @@ -59,6 +61,15 @@ extension MagicCacheServiceExtension on MagicCache { ResultType.magic, title, enteFilesInMagicCache, + onResultTap: (ctx) { + routeToPage( + ctx, + MagicResultScreen( + enteFilesInMagicCache, + name: title, + ), + ); + }, ); } } 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 b0a35613ea..377a1d8659 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import "package:photos/core/network/network.dart"; import "package:photos/db/files_db.dart"; +import "package:photos/events/magic_sort_change_event.dart"; import 'package:photos/events/subscription_purchased_event.dart'; import "package:photos/gateways/cast_gw.dart"; import "package:photos/generated/l10n.dart"; @@ -58,11 +59,11 @@ class GalleryAppBarWidget extends StatefulWidget { this.type, this.title, this.selectedFiles, { - Key? key, + super.key, this.deviceCollection, this.collection, this.isFromCollectPhotos = false, - }) : super(key: key); + }); @override State createState() => _GalleryAppBarWidgetState(); @@ -84,6 +85,8 @@ enum AlbumPopupAction { pinAlbum, removeLink, cleanUncategorized, + sortByMostRecent, + sortByMostRelevant } class _GalleryAppBarWidgetState extends State { @@ -293,6 +296,39 @@ class _GalleryAppBarWidgetState extends State { !Configuration.instance.hasConfiguredAccount()) { return actions; } + + if (galleryType == GalleryType.magic) { + actions.add( + Tooltip( + message: "Sort", + child: PopupMenuButton( + icon: const Icon(Icons.sort_rounded), + itemBuilder: (context) { + return const [ + PopupMenuItem( + value: AlbumPopupAction.sortByMostRecent, + child: Text("Most recent"), + ), + PopupMenuItem( + value: AlbumPopupAction.sortByMostRelevant, + child: Text("Most relevant"), + ), + ]; + }, + onSelected: (AlbumPopupAction value) { + if (value == AlbumPopupAction.sortByMostRecent) { + Bus.instance + .fire(MagicSortChangeEvent(MagicSortType.mostRecent)); + } else if (value == AlbumPopupAction.sortByMostRelevant) { + Bus.instance + .fire(MagicSortChangeEvent(MagicSortType.mostRelevant)); + } + }, + ), + ), + ); + } + final int userID = Configuration.instance.getUserID()!; isQuickLink = widget.collection?.isQuickLinkCollection() ?? false; if (galleryType.canAddFiles(widget.collection, userID)) { diff --git a/mobile/lib/ui/viewer/search/result/magic_result_screen.dart b/mobile/lib/ui/viewer/search/result/magic_result_screen.dart new file mode 100644 index 0000000000..4c0b410e4c --- /dev/null +++ b/mobile/lib/ui/viewer/search/result/magic_result_screen.dart @@ -0,0 +1,141 @@ +import "dart:async"; + +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/events/magic_sort_change_event.dart"; +import 'package:photos/models/file/file.dart'; +import 'package:photos/models/file_load_result.dart'; +import 'package:photos/models/gallery_type.dart'; +import 'package:photos/models/selected_files.dart'; +import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.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/state/selection_state.dart"; + +class MagicResultScreen extends StatefulWidget { + final List files; + final String name; + final bool enableGrouping; + + static const GalleryType appBarType = GalleryType.magic; + static const GalleryType overlayType = GalleryType.magic; + + const MagicResultScreen( + this.files, { + required this.name, + this.enableGrouping = true, + super.key, + }); + + @override + State createState() => _MagicResultScreenState(); +} + +class _MagicResultScreenState extends State { + final _selectedFiles = SelectedFiles(); + late final List files; + late final StreamSubscription _filesUpdatedEvent; + late final StreamSubscription _magicSortChangeEvent; + late bool _enableGrouping; + + @override + void initState() { + super.initState(); + _enableGrouping = widget.enableGrouping; + files = widget.files; + _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) { + files.remove(updatedFile); + } + setState(() {}); + } + }); + + _magicSortChangeEvent = + Bus.instance.on().listen((event) { + if (event.sortType == MagicSortType.mostRelevant) { + setState(() { + _enableGrouping = false; + }); + } else if (event.sortType == MagicSortType.mostRecent) { + setState(() { + _enableGrouping = true; + }); + } + }); + } + + @override + void dispose() { + _filesUpdatedEvent.cancel(); + _magicSortChangeEvent.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final gallery = Gallery( + key: ValueKey(_enableGrouping), + asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { + final result = files + .where( + (file) => + file.creationTime! >= creationStartTime && + file.creationTime! <= creationEndTime, + ) + .toList(); + return Future.value( + FileLoadResult( + result, + result.length < files.length, + ), + ); + }, + reloadEvent: Bus.instance.on(), + removalEventTypes: const { + EventType.deletedFromRemote, + EventType.deletedFromEverywhere, + EventType.hide, + }, + tagPrefix: "", + selectedFiles: _selectedFiles, + enableFileGrouping: _enableGrouping, + initialFiles: [files.first], + ); + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: GalleryAppBarWidget( + MagicResultScreen.appBarType, + widget.name, + _selectedFiles, + ), + ), + body: SelectionState( + selectedFiles: _selectedFiles, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeInOutQuad, + switchOutCurve: Curves.easeInOutQuad, + child: gallery, + ), + FileSelectionOverlayBar( + MagicResultScreen.overlayType, + _selectedFiles, + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/search/result/search_result_widget.dart b/mobile/lib/ui/viewer/search/result/search_result_widget.dart index 965e0fcc0e..6dacab459a 100644 --- a/mobile/lib/ui/viewer/search/result/search_result_widget.dart +++ b/mobile/lib/ui/viewer/search/result/search_result_widget.dart @@ -110,7 +110,6 @@ class SearchResultWidget extends StatelessWidget { context, SearchResultPage( searchResult, - enableGrouping: searchResult.type() != ResultType.magic, ), ); } diff --git a/mobile/lib/ui/viewer/search_tab/magic_section.dart b/mobile/lib/ui/viewer/search_tab/magic_section.dart index 97996a433d..6ff1561cd4 100644 --- a/mobile/lib/ui/viewer/search_tab/magic_section.dart +++ b/mobile/lib/ui/viewer/search_tab/magic_section.dart @@ -11,9 +11,7 @@ import "package:photos/models/search/search_types.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; -import "package:photos/ui/viewer/search/result/search_result_page.dart"; import "package:photos/ui/viewer/search_tab/section_header.dart"; -import "package:photos/utils/navigation_util.dart"; class MagicSection extends StatefulWidget { final List magicSearchResults; @@ -149,17 +147,8 @@ class MagicRecommendation extends StatelessWidget { child: GestureDetector( onTap: () { RecentSearches().add(magicSearchResult.name()); - if (magicSearchResult.onResultTap != null) { - magicSearchResult.onResultTap!(context); - } else { - routeToPage( - context, - SearchResultPage( - magicSearchResult, - enableGrouping: false, - ), - ); - } + + magicSearchResult.onResultTap!(context); }, child: SizedBox( width: _width + _borderWidth * 2,