瀏覽代碼

Merge branch 'master' into reupload_hash_check

Neeraj Gupta 2 年之前
父節點
當前提交
74734d47f2
共有 70 個文件被更改,包括 2525 次插入877 次删除
  1. 2 0
      analysis_options.yaml
  2. 6 2
      lib/core/configuration.dart
  3. 2 0
      lib/core/constants.dart
  4. 8 0
      lib/data/holidays.dart
  5. 16 0
      lib/data/months.dart
  6. 23 0
      lib/data/years.dart
  7. 13 4
      lib/db/files_db.dart
  8. 8 0
      lib/ente_theme_data.dart
  9. 2 0
      lib/main.dart
  10. 10 13
      lib/models/file.dart
  11. 8 0
      lib/models/search/album_search_result.dart
  12. 8 0
      lib/models/search/file_search_result.dart
  13. 15 0
      lib/models/search/holiday_search_result.dart
  14. 52 0
      lib/models/search/location_api_response.dart
  15. 9 0
      lib/models/search/location_search_result.dart
  16. 14 0
      lib/models/search/month_search_result.dart
  17. 1 0
      lib/models/search/search_results.dart
  18. 9 0
      lib/models/search/year_search_result.dart
  19. 0 5
      lib/models/search_result.dart
  20. 0 41
      lib/services/collections_service.dart
  21. 267 0
      lib/services/search_service.dart
  22. 10 10
      lib/ui/account/delete_account_page.dart
  23. 91 0
      lib/ui/collections/collection_item_widget.dart
  24. 45 0
      lib/ui/collections/create_new_album_widget.dart
  25. 98 0
      lib/ui/collections/device_folder_icon_widget.dart
  26. 39 0
      lib/ui/collections/device_folders_grid_view_widget.dart
  27. 47 0
      lib/ui/collections/ente_section_title.dart
  28. 102 0
      lib/ui/collections/hidden_collections_button_widget.dart
  29. 50 0
      lib/ui/collections/remote_collections_grid_view_widget.dart
  30. 33 0
      lib/ui/collections/section_title.dart
  31. 97 0
      lib/ui/collections/trash_button_widget.dart
  32. 11 533
      lib/ui/collections_gallery_widget.dart
  33. 1 0
      lib/ui/home_widget.dart
  34. 15 13
      lib/ui/huge_listview/draggable_scrollbar.dart
  35. 4 0
      lib/ui/huge_listview/huge_listview.dart
  36. 1 1
      lib/ui/huge_listview/lazy_loading_gallery.dart
  37. 0 4
      lib/ui/nav_bar.dart
  38. 6 6
      lib/ui/payment/subscription_page.dart
  39. 1 1
      lib/ui/shared_collections_gallery.dart
  40. 9 11
      lib/ui/status_bar_widget.dart
  41. 3 37
      lib/ui/viewer/file/fading_app_bar.dart
  42. 1 1
      lib/ui/viewer/file/zoomable_live_image.dart
  43. 0 1
      lib/ui/viewer/gallery/archive_page.dart
  44. 0 1
      lib/ui/viewer/gallery/collection_page.dart
  45. 0 1
      lib/ui/viewer/gallery/device_all_page.dart
  46. 0 1
      lib/ui/viewer/gallery/device_folder_page.dart
  47. 4 1
      lib/ui/viewer/gallery/gallery.dart
  48. 2 1
      lib/ui/viewer/gallery/gallery_overlay_widget.dart
  49. 0 1
      lib/ui/viewer/gallery/trash_page.dart
  50. 0 75
      lib/ui/viewer/search/collection_result_widget.dart
  51. 75 0
      lib/ui/viewer/search/collections/files_from_holiday_page.dart
  52. 75 0
      lib/ui/viewer/search/collections/files_from_month_page.dart
  53. 75 0
      lib/ui/viewer/search/collections/files_from_year_page.dart
  54. 75 0
      lib/ui/viewer/search/collections/files_in_location_page.dart
  55. 102 0
      lib/ui/viewer/search/search_result_widgets/collection_result_widget.dart
  56. 77 0
      lib/ui/viewer/search/search_result_widgets/file_result_widget.dart
  57. 89 0
      lib/ui/viewer/search/search_result_widgets/holiday_result_widget.dart
  58. 89 0
      lib/ui/viewer/search/search_result_widgets/location_result_widget.dart
  59. 89 0
      lib/ui/viewer/search/search_result_widgets/month_result_widget.dart
  60. 77 0
      lib/ui/viewer/search/search_result_widgets/no_result_widget.dart
  61. 31 0
      lib/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart
  62. 85 0
      lib/ui/viewer/search/search_result_widgets/year_result_widget.dart
  63. 0 29
      lib/ui/viewer/search/search_results_suggestions.dart
  64. 60 0
      lib/ui/viewer/search/search_suffix_icon_widget.dart
  65. 87 0
      lib/ui/viewer/search/search_suggestions.dart
  66. 166 81
      lib/ui/viewer/search/search_widget.dart
  67. 2 0
      lib/utils/date_time_util.dart
  68. 95 2
      lib/utils/navigation_util.dart
  69. 32 0
      lib/utils/search_debouncer.dart
  70. 1 1
      pubspec.yaml

+ 2 - 0
analysis_options.yaml

@@ -6,6 +6,7 @@ include: package:flutter_lints/flutter.yaml
 linter:
   rules:
     # Ref https://github.com/flutter/packages/blob/master/packages/flutter_lints/lib/flutter.yaml
+    # Ref https://dart-lang.github.io/linter/lints/
     - avoid_print
     - avoid_unnecessary_containers
     - avoid_web_libraries_in_flutter
@@ -29,6 +30,7 @@ linter:
     - prefer_double_quotes
     - directives_ordering
     - always_use_package_imports
+    - sort_child_properties_last
 
 analyzer:
   errors:

+ 6 - 2
lib/core/configuration.dart

@@ -26,6 +26,7 @@ import 'package:photos/services/billing_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/favorites_service.dart';
 import 'package:photos/services/memories_service.dart';
+import 'package:photos/services/search_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -36,8 +37,10 @@ class Configuration {
   Configuration._privateConstructor();
 
   static final Configuration instance = Configuration._privateConstructor();
-  static const endpoint =
-      String.fromEnvironment("endpoint", defaultValue: "https://api.ente.io");
+  static const endpoint = String.fromEnvironment(
+    "endpoint",
+    defaultValue: kDefaultProductionEndpoint,
+  );
   static const emailKey = "email";
   static const foldersToBackUpKey = "folders_to_back_up";
   static const keyAttributesKey = "key_attributes";
@@ -158,6 +161,7 @@ class Configuration {
     FavoritesService.instance.clearCache();
     MemoriesService.instance.clearCache();
     BillingService.instance.clearCache();
+    SearchService.instance.clearCache();
     Bus.instance.fire(UserLoggedOutEvent());
   }
 

+ 2 - 0
lib/core/constants.dart

@@ -41,3 +41,5 @@ class FFDefault {
   static const bool disableCFWorker = false;
   static const bool enableMissingLocationMigration = false;
 }
+
+const kDefaultProductionEndpoint = 'https://api.ente.io';

+ 8 - 0
lib/data/holidays.dart

@@ -0,0 +1,8 @@
+import 'package:photos/models/search/holiday_search_result.dart';
+
+const List<HolidayData> allHolidays = [
+  HolidayData('Christmas', 12, 25),
+  HolidayData('Christmas Eve', 12, 24),
+  HolidayData('New Year', 1, 1),
+  HolidayData('New Year Eve', 12, 31),
+];

+ 16 - 0
lib/data/months.dart

@@ -0,0 +1,16 @@
+import 'package:photos/models/search/month_search_result.dart';
+
+List<MonthData> allMonths = [
+  MonthData('January', 1),
+  MonthData('February', 2),
+  MonthData('March', 3),
+  MonthData('April', 4),
+  MonthData('May', 5),
+  MonthData('June', 6),
+  MonthData('July', 7),
+  MonthData('August', 8),
+  MonthData('September', 9),
+  MonthData('October', 10),
+  MonthData('November', 11),
+  MonthData('December', 12),
+];

+ 23 - 0
lib/data/years.dart

@@ -0,0 +1,23 @@
+import 'package:photos/utils/date_time_util.dart';
+
+class YearsData {
+  final List<YearData> yearsData = [];
+  YearsData._privateConstructor() {
+    for (int year = currentYear; year >= 1970; year--) {
+      //appending from the latest year so that search results will show latest year first
+      yearsData.add(
+        YearData(year.toString(), [
+          DateTime(year).microsecondsSinceEpoch,
+          DateTime(year + 1).microsecondsSinceEpoch,
+        ]),
+      );
+    }
+  }
+  static final YearsData instance = YearsData._privateConstructor();
+}
+
+class YearData {
+  final String year;
+  final List<int> duration;
+  YearData(this.year, this.duration);
+}

+ 13 - 4
lib/db/files_db.dart

@@ -80,7 +80,6 @@ class FilesDB {
     initializationScript: initializationScript,
     migrationScripts: migrationScripts,
   );
-
   // make this a singleton class
   FilesDB._privateConstructor();
 
@@ -617,8 +616,9 @@ class FilesDB {
 
   Future<List<File>> getFilesCreatedWithinDurations(
     List<List<int>> durations,
-    Set<int> ignoredCollectionIDs,
-  ) async {
+    Set<int> ignoredCollectionIDs, {
+    String order = 'ASC',
+  }) async {
     final db = await instance.database;
     String whereClause = "( ";
     for (int index = 0; index < durations.length; index++) {
@@ -635,7 +635,7 @@ class FilesDB {
     final results = await db.query(
       table,
       where: whereClause,
-      orderBy: '$columnCreationTime ASC',
+      orderBy: '$columnCreationTime ' + order,
     );
     final files = _convertToFiles(results);
     return _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
@@ -1162,6 +1162,15 @@ class FilesDB {
     return files;
   }
 
+  Future<List<File>> getAllFilesFromDB() async {
+    final db = await instance.database;
+    List<Map<String, dynamic>> result = await db.query(table);
+    List<File> files = _convertToFiles(result);
+    List<File> deduplicatedFiles =
+        _deduplicatedAndFilterIgnoredFiles(files, null);
+    return deduplicatedFiles;
+  }
+
   Map<String, dynamic> _getRowForFile(File file) {
     final row = <String, dynamic>{};
     if (file.generatedID != null) {

+ 8 - 0
lib/ente_theme_data.dart

@@ -349,6 +349,14 @@ extension CustomColorScheme on ColorScheme {
   Color get themeSwitchInactiveIconColor => brightness == Brightness.light
       ? Colors.black.withOpacity(0.5)
       : Colors.white.withOpacity(0.5);
+
+  Color get searchResultsColor => brightness == Brightness.light
+      ? const Color.fromRGBO(245, 245, 245, 1.0)
+      : const Color.fromRGBO(30, 30, 30, 1.0);
+
+  Color get searchResultsCountTextColor => brightness == Brightness.light
+      ? const Color.fromRGBO(80, 80, 80, 1)
+      : const Color.fromRGBO(150, 150, 150, 1);
 }
 
 OutlinedButtonThemeData buildOutlinedButtonThemeData({

+ 2 - 0
lib/main.dart

@@ -25,6 +25,7 @@ import 'package:photos/services/memories_service.dart';
 import 'package:photos/services/notification_service.dart';
 import 'package:photos/services/push_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
+import 'package:photos/services/search_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/trash_sync_service.dart';
 import 'package:photos/services/update_service.dart';
@@ -141,6 +142,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
   await MemoriesService.instance.init();
   await LocalSettings.instance.init();
   await LocalFileUpdateService.instance.init();
+  await SearchService.instance.init();
   if (Platform.isIOS) {
     PushService.instance.init().then((_) {
       FirebaseMessaging.onBackgroundMessage(

+ 10 - 13
lib/models/file.dart

@@ -1,4 +1,3 @@
-import 'package:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.dart';
@@ -195,24 +194,22 @@ class File extends EnteFile {
   }
 
   String getDownloadUrl() {
-    if (kDebugMode || FeatureFlagService.instance.disableCFWorker()) {
-      return Configuration.instance.getHttpEndpoint() +
-          "/files/download/" +
-          uploadedFileID.toString();
+    final endpoint = Configuration.instance.getHttpEndpoint();
+    if (endpoint != kDefaultProductionEndpoint ||
+        FeatureFlagService.instance.disableCFWorker()) {
+      return endpoint + "/files/download/" + uploadedFileID.toString();
     } else {
-      return "https://files.ente.workers.dev/?fileID=" +
-          uploadedFileID.toString();
+      return "https://files.ente.io/?fileID=" + uploadedFileID.toString();
     }
   }
 
   String getThumbnailUrl() {
-    if (kDebugMode || FeatureFlagService.instance.disableCFWorker()) {
-      return Configuration.instance.getHttpEndpoint() +
-          "/files/preview/" +
-          uploadedFileID.toString();
+    final endpoint = Configuration.instance.getHttpEndpoint();
+    if (endpoint != kDefaultProductionEndpoint ||
+        FeatureFlagService.instance.disableCFWorker()) {
+      return endpoint + "/files/preview/" + uploadedFileID.toString();
     } else {
-      return "https://thumbnails.ente.workers.dev/?fileID=" +
-          uploadedFileID.toString();
+      return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString();
     }
   }
 

+ 8 - 0
lib/models/search/album_search_result.dart

@@ -0,0 +1,8 @@
+import 'package:photos/models/collection_items.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class AlbumSearchResult extends SearchResult {
+  final CollectionWithThumbnail collectionWithThumbnail;
+
+  AlbumSearchResult(this.collectionWithThumbnail);
+}

+ 8 - 0
lib/models/search/file_search_result.dart

@@ -0,0 +1,8 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class FileSearchResult extends SearchResult {
+  final File file;
+
+  FileSearchResult(this.file);
+}

+ 15 - 0
lib/models/search/holiday_search_result.dart

@@ -0,0 +1,15 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class HolidaySearchResult extends SearchResult {
+  final String holidayName;
+  final List<File> files;
+  HolidaySearchResult(this.holidayName, this.files);
+}
+
+class HolidayData {
+  final String name;
+  final int month;
+  final int day;
+  const HolidayData(this.name, this.month, this.day);
+}

+ 52 - 0
lib/models/search/location_api_response.dart

@@ -0,0 +1,52 @@
+class LocationApiResponse {
+  final List<LocationDataFromResponse> results;
+  LocationApiResponse({
+    this.results,
+  });
+
+  LocationApiResponse copyWith({
+    List<LocationDataFromResponse> results,
+  }) {
+    return LocationApiResponse(
+      results: results ?? this.results,
+    );
+  }
+
+  factory LocationApiResponse.fromMap(Map<String, dynamic> map) {
+    return LocationApiResponse(
+      results: List<LocationDataFromResponse>.from(
+        (map['results']).map(
+          (x) => LocationDataFromResponse.fromMap(x as Map<String, dynamic>),
+        ),
+      ),
+    );
+  }
+}
+
+class LocationDataFromResponse {
+  final String place;
+  final List<double> bbox;
+  LocationDataFromResponse({
+    this.place,
+    this.bbox,
+  });
+
+  LocationDataFromResponse copyWith({
+    String place,
+    List<double> bbox,
+  }) {
+    return LocationDataFromResponse(
+      place: place ?? this.place,
+      bbox: bbox ?? this.bbox,
+    );
+  }
+
+  factory LocationDataFromResponse.fromMap(Map<String, dynamic> map) {
+    return LocationDataFromResponse(
+      place: map['place'] as String,
+      bbox: List<double>.from(
+        (map['bbox']),
+      ),
+    );
+  }
+}

+ 9 - 0
lib/models/search/location_search_result.dart

@@ -0,0 +1,9 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class LocationSearchResult extends SearchResult {
+  final String location;
+  final List<File> files;
+
+  LocationSearchResult(this.location, this.files);
+}

+ 14 - 0
lib/models/search/month_search_result.dart

@@ -0,0 +1,14 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class MonthSearchResult extends SearchResult {
+  final String month;
+  final List<File> files;
+  MonthSearchResult(this.month, this.files);
+}
+
+class MonthData {
+  final String name;
+  final int monthNumber;
+  MonthData(this.name, this.monthNumber);
+}

+ 1 - 0
lib/models/search/search_results.dart

@@ -0,0 +1 @@
+class SearchResult {}

+ 9 - 0
lib/models/search/year_search_result.dart

@@ -0,0 +1,9 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_results.dart';
+
+class YearSearchResult extends SearchResult {
+  final String year;
+  final List<File> files;
+
+  YearSearchResult(this.year, this.files);
+}

+ 0 - 5
lib/models/search_result.dart

@@ -1,5 +0,0 @@
-class SearchResult {
-  final String path;
-
-  SearchResult(this.path);
-}

+ 0 - 41
lib/services/collections_service.dart

@@ -19,7 +19,6 @@ import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_file_item.dart';
-import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
@@ -171,46 +170,6 @@ class CollectionsService {
         .toList();
   }
 
-  // getFilteredCollectionsWithThumbnail removes deleted or archived or
-  // collections which don't have a file from search result
-  Future<List<CollectionWithThumbnail>> getFilteredCollectionsWithThumbnail(
-    String query,
-  ) async {
-    // identify collections which have at least one file as we don't display
-    // empty collection
-
-    List<File> latestCollectionFiles = await getLatestCollectionFiles();
-    Map<int, File> collectionIDToLatestFileMap = {
-      for (File file in latestCollectionFiles) file.collectionID: file
-    };
-
-    /* Identify collections whose name matches the search query
-      and is not archived
-      and is not deleted
-      and has at-least one file
-     */
-
-    List<Collection> matchedCollection = _collectionIDToCollections.values
-        .where(
-          (c) =>
-              !c.isDeleted && // not deleted
-              !c.isArchived() // not archived
-              &&
-              collectionIDToLatestFileMap.containsKey(c.id) && // the
-              // collection is not empty
-              c.name.contains(RegExp(query, caseSensitive: false)),
-        )
-        .toList();
-    List<CollectionWithThumbnail> result = [];
-    for (Collection collection in matchedCollection) {
-      result.add(CollectionWithThumbnail(
-        collection,
-        collectionIDToLatestFileMap[collection.id],
-      ));
-    }
-    return result;
-  }
-
   Future<List<User>> getSharees(int collectionID) {
     return _dio
         .get(

+ 267 - 0
lib/services/search_service.dart

@@ -0,0 +1,267 @@
+import 'package:dio/dio.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/core/network.dart';
+import 'package:photos/data/holidays.dart';
+import 'package:photos/data/months.dart';
+import 'package:photos/data/years.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/models/collection_items.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/location.dart';
+import 'package:photos/models/search/album_search_result.dart';
+import 'package:photos/models/search/holiday_search_result.dart';
+import 'package:photos/models/search/location_api_response.dart';
+import 'package:photos/models/search/location_search_result.dart';
+import 'package:photos/models/search/month_search_result.dart';
+import 'package:photos/models/search/year_search_result.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/utils/date_time_util.dart';
+
+class SearchService {
+  Future<List<File>> _cachedFilesFuture;
+  final _dio = Network.instance.getDio();
+  final _config = Configuration.instance;
+  final _logger = Logger((SearchService).toString());
+  final _collectionService = CollectionsService.instance;
+  static const _maximumResultsLimit = 20;
+
+  SearchService._privateConstructor();
+  static final SearchService instance = SearchService._privateConstructor();
+
+  Future<void> init() async {
+    // Intention of delay is to give more CPU cycles to other tasks
+    Future.delayed(const Duration(seconds: 5), () async {
+      /* In case home screen loads before 5 seconds and user starts search,
+       future will not be null.So here getAllFiles won't run again in that case. */
+      if (_cachedFilesFuture == null) {
+        getAllFiles();
+      }
+    });
+
+    Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
+      _cachedFilesFuture = null;
+      getAllFiles();
+    });
+  }
+
+  Future<List<File>> getAllFiles() async {
+    if (_cachedFilesFuture != null) {
+      return _cachedFilesFuture;
+    }
+    _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
+    return _cachedFilesFuture;
+  }
+
+  Future<List<File>> getFileSearchResults(String query) async {
+    final List<File> fileSearchResults = [];
+    final List<File> files = await getAllFiles();
+    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+    for (var file in files) {
+      if (fileSearchResults.length >= _maximumResultsLimit) {
+        break;
+      }
+      if (file.title.contains(nonCaseSensitiveRegexForQuery)) {
+        fileSearchResults.add(file);
+      }
+    }
+    return fileSearchResults;
+  }
+
+  void clearCache() {
+    _cachedFilesFuture = null;
+  }
+
+  Future<List<LocationSearchResult>> getLocationSearchResults(
+    String query,
+  ) async {
+    final List<LocationSearchResult> locationSearchResults = [];
+    try {
+      final List<File> allFiles = await SearchService.instance.getAllFiles();
+
+      final response = await _dio.get(
+        _config.getHttpEndpoint() + "/search/location",
+        queryParameters: {"query": query, "limit": 10},
+        options: Options(
+          headers: {"X-Auth-Token": _config.getToken()},
+        ),
+      );
+
+      final matchedLocationSearchResults =
+          LocationApiResponse.fromMap(response.data);
+
+      for (var locationData in matchedLocationSearchResults.results) {
+        final List<File> filesInLocation = [];
+
+        for (var file in allFiles) {
+          if (_isValidLocation(file.location) &&
+              _isLocationWithinBounds(file.location, locationData)) {
+            filesInLocation.add(file);
+          }
+        }
+        filesInLocation.sort(
+          (first, second) => second.creationTime.compareTo(first.creationTime),
+        );
+        if (filesInLocation.isNotEmpty) {
+          locationSearchResults.add(
+            LocationSearchResult(locationData.place, filesInLocation),
+          );
+        }
+      }
+    } catch (e) {
+      _logger.severe(e);
+    }
+    return locationSearchResults;
+  }
+
+  // getFilteredCollectionsWithThumbnail removes deleted or archived or
+  // collections which don't have a file from search result
+  Future<List<AlbumSearchResult>> getCollectionSearchResults(
+    String query,
+  ) async {
+    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+
+    /*latestCollectionFiles is to identify collections which have at least one file as we don't display
+     empty collections and to get the file to pass for tumbnail */
+    final List<File> latestCollectionFiles =
+        await _collectionService.getLatestCollectionFiles();
+
+    final List<AlbumSearchResult> collectionSearchResults = [];
+
+    for (var file in latestCollectionFiles) {
+      if (collectionSearchResults.length >= _maximumResultsLimit) {
+        break;
+      }
+      final Collection collection =
+          CollectionsService.instance.getCollectionByID(file.collectionID);
+      if (!collection.isArchived() &&
+          collection.name.contains(nonCaseSensitiveRegexForQuery)) {
+        collectionSearchResults
+            .add(AlbumSearchResult(CollectionWithThumbnail(collection, file)));
+      }
+    }
+
+    return collectionSearchResults;
+  }
+
+  Future<List<YearSearchResult>> getYearSearchResults(
+    String yearFromQuery,
+  ) async {
+    final List<YearSearchResult> yearSearchResults = [];
+    for (var yearData in YearsData.instance.yearsData) {
+      if (yearData.year.startsWith(yearFromQuery)) {
+        final List<File> filesInYear = await _getFilesInYear(yearData.duration);
+        if (filesInYear.isNotEmpty) {
+          yearSearchResults.add(
+            YearSearchResult(
+              yearData.year,
+              filesInYear,
+            ),
+          );
+        }
+      }
+    }
+    return yearSearchResults;
+  }
+
+  Future<List<HolidaySearchResult>> getHolidaySearchResults(
+    String query,
+  ) async {
+    final List<HolidaySearchResult> holidaySearchResults = [];
+
+    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+
+    for (var holiday in allHolidays) {
+      if (holiday.name.contains(nonCaseSensitiveRegexForQuery)) {
+        final matchedFiles =
+            await FilesDB.instance.getFilesCreatedWithinDurations(
+          _getDurationsOfHolidayInEveryYear(holiday.day, holiday.month),
+          null,
+          order: 'DESC',
+        );
+        if (matchedFiles.isNotEmpty) {
+          holidaySearchResults.add(
+            HolidaySearchResult(holiday.name, matchedFiles),
+          );
+        }
+      }
+    }
+    return holidaySearchResults;
+  }
+
+  Future<List<MonthSearchResult>> getMonthSearchResults(String query) async {
+    final List<MonthSearchResult> monthSearchResults = [];
+    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+
+    for (var month in allMonths) {
+      if (month.name.startsWith(nonCaseSensitiveRegexForQuery)) {
+        monthSearchResults.add(
+          MonthSearchResult(
+            month.name,
+            await FilesDB.instance.getFilesCreatedWithinDurations(
+              _getDurationsOfMonthInEveryYear(month.monthNumber),
+              null,
+              order: 'DESC',
+            ),
+          ),
+        );
+      }
+    }
+
+    return monthSearchResults;
+  }
+
+  Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
+    return await FilesDB.instance.getFilesCreatedWithinDurations(
+      [durationOfYear],
+      null,
+      order: "DESC",
+    );
+  }
+
+  List<List<int>> _getDurationsOfHolidayInEveryYear(int day, int month) {
+    final List<List<int>> durationsOfHolidayInEveryYear = [];
+    for (var year = 1970; year <= currentYear; year++) {
+      durationsOfHolidayInEveryYear.add([
+        DateTime(year, month, day).microsecondsSinceEpoch,
+        DateTime(year, month, day + 1).microsecondsSinceEpoch,
+      ]);
+    }
+    return durationsOfHolidayInEveryYear;
+  }
+
+  List<List<int>> _getDurationsOfMonthInEveryYear(int month) {
+    final List<List<int>> durationsOfMonthInEveryYear = [];
+    for (var year = 1970; year < currentYear; year++) {
+      durationsOfMonthInEveryYear.add([
+        DateTime.utc(year, month, 1).microsecondsSinceEpoch,
+        month == 12
+            ? DateTime(year + 1, 1, 1).microsecondsSinceEpoch
+            : DateTime(year, month + 1, 1).microsecondsSinceEpoch,
+      ]);
+    }
+    return durationsOfMonthInEveryYear;
+  }
+
+  bool _isValidLocation(Location location) {
+    return location != null &&
+        location.latitude != null &&
+        location.latitude != 0 &&
+        location.longitude != null &&
+        location.longitude != 0;
+  }
+
+  bool _isLocationWithinBounds(
+    Location location,
+    LocationDataFromResponse locationData,
+  ) {
+    //format returned by the api is [lng,lat,lng,lat] where indexes 0 & 1 are southwest and 2 & 3 northeast
+    return location.longitude > locationData.bbox[0] &&
+        location.latitude > locationData.bbox[1] &&
+        location.longitude < locationData.bbox[2] &&
+        location.latitude < locationData.bbox[3];
+  }
+}

+ 10 - 10
lib/ui/account/delete_account_page.dart

@@ -128,16 +128,6 @@ class DeleteAccountPage extends StatelessWidget {
   }
 
   Future<void> _initiateDelete(BuildContext context) async {
-    AppLock.of(context).setEnabled(false);
-    String reason = "Please authenticate to initiate account deletion";
-    final result = await requestAuthentication(reason);
-    AppLock.of(context).setEnabled(
-      Configuration.instance.shouldShowLockScreen(),
-    );
-    if (!result) {
-      showToast(context, reason);
-      return;
-    }
     final deleteChallengeResponse =
         await UserService.instance.getDeleteChallenge(context);
     if (deleteChallengeResponse == null) {
@@ -154,6 +144,16 @@ class DeleteAccountPage extends StatelessWidget {
     BuildContext context,
     DeleteChallengeResponse response,
   ) async {
+    AppLock.of(context).setEnabled(false);
+    String reason = "Please authenticate to initiate account deletion";
+    final result = await requestAuthentication(reason);
+    AppLock.of(context).setEnabled(
+      Configuration.instance.shouldShowLockScreen(),
+    );
+    if (!result) {
+      showToast(context, reason);
+      return;
+    }
     final choice = await showChoiceDialog(
       context,
       'Are you sure you want to delete your account?',

+ 91 - 0
lib/ui/collections/collection_item_widget.dart

@@ -0,0 +1,91 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/collection_items.dart';
+import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
+import 'package:photos/ui/viewer/gallery/collection_page.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class CollectionItem extends StatelessWidget {
+  CollectionItem(
+    this.c, {
+    Key key,
+  }) : super(key: Key(c.collection.id.toString()));
+
+  final CollectionWithThumbnail c;
+
+  @override
+  Widget build(BuildContext context) {
+    const double horizontalPaddingOfGridRow = 16;
+    const double crossAxisSpacingOfGrid = 9;
+    Size size = MediaQuery.of(context).size;
+    int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
+    double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
+        (albumsCountInOneRow - 1) * crossAxisSpacingOfGrid;
+    TextStyle albumTitleTextStyle =
+        Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14);
+    final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
+        (totalWhiteSpaceOfRow / albumsCountInOneRow);
+    return GestureDetector(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: <Widget>[
+          ClipRRect(
+            borderRadius: BorderRadius.circular(4),
+            child: SizedBox(
+              height: sideOfThumbnail,
+              width: sideOfThumbnail,
+              child: Hero(
+                tag: "collection" + c.thumbnail.tag(),
+                child: ThumbnailWidget(
+                  c.thumbnail,
+                  shouldShowArchiveStatus: c.collection.isArchived(),
+                  key: Key(
+                    "collection" + c.thumbnail.tag(),
+                  ),
+                ),
+              ),
+            ),
+          ),
+          const SizedBox(height: 4),
+          Row(
+            children: [
+              Container(
+                constraints: BoxConstraints(maxWidth: sideOfThumbnail - 40),
+                child: Text(
+                  c.collection.name,
+                  style: albumTitleTextStyle,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ),
+              FutureBuilder<int>(
+                future: FilesDB.instance.collectionFileCount(c.collection.id),
+                builder: (context, snapshot) {
+                  if (snapshot.hasData && snapshot.data > 0) {
+                    return RichText(
+                      text: TextSpan(
+                        style: albumTitleTextStyle.copyWith(
+                          color: albumTitleTextStyle.color.withOpacity(0.5),
+                        ),
+                        children: [
+                          const TextSpan(text: "  \u2022  "),
+                          TextSpan(text: snapshot.data.toString()),
+                        ],
+                      ),
+                    );
+                  } else {
+                    return const SizedBox.shrink();
+                  }
+                },
+              ),
+            ],
+          ),
+        ],
+      ),
+      onTap: () {
+        routeToPage(context, CollectionPage(c));
+      },
+    );
+  }
+}

+ 45 - 0
lib/ui/collections/create_new_album_widget.dart

@@ -0,0 +1,45 @@
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/tab_changed_event.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class CreateNewAlbumWidget extends StatelessWidget {
+  const CreateNewAlbumWidget({
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      child: Container(
+        margin: const EdgeInsets.fromLTRB(30, 30, 30, 54),
+        decoration: BoxDecoration(
+          color: Theme.of(context).backgroundColor,
+          boxShadow: [
+            BoxShadow(
+              blurRadius: 2,
+              spreadRadius: 0,
+              offset: const Offset(0, 0),
+              color: Theme.of(context).iconTheme.color.withOpacity(0.3),
+            )
+          ],
+          borderRadius: BorderRadius.circular(4),
+        ),
+        child: Icon(
+          Icons.add,
+          color: Theme.of(context).iconTheme.color.withOpacity(0.25),
+        ),
+      ),
+      onTap: () async {
+        await showToast(
+          context,
+          "Long press to select photos and click + to create an album",
+          toastLength: Toast.LENGTH_LONG,
+        );
+        Bus.instance
+            .fire(TabChangedEvent(0, TabChangedEventSource.collectionsPage));
+      },
+    );
+  }
+}

+ 98 - 0
lib/ui/collections/device_folder_icon_widget.dart

@@ -0,0 +1,98 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/models/device_folder.dart';
+import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
+import 'package:photos/ui/viewer/gallery/device_folder_page.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class DeviceFolderIcon extends StatelessWidget {
+  const DeviceFolderIcon(
+    this.folder, {
+    Key key,
+  }) : super(key: key);
+
+  static final kUnsyncedIconOverlay = Container(
+    decoration: BoxDecoration(
+      gradient: LinearGradient(
+        begin: Alignment.topCenter,
+        end: Alignment.bottomCenter,
+        colors: [
+          Colors.transparent,
+          Colors.black.withOpacity(0.6),
+        ],
+        stops: const [0.7, 1],
+      ),
+    ),
+    child: Align(
+      alignment: Alignment.bottomRight,
+      child: Padding(
+        padding: const EdgeInsets.only(right: 8, bottom: 8),
+        child: Icon(
+          Icons.cloud_off_outlined,
+          size: 18,
+          color: Colors.white.withOpacity(0.9),
+        ),
+      ),
+    ),
+  );
+
+  final DeviceFolder folder;
+
+  @override
+  Widget build(BuildContext context) {
+    final isBackedUp =
+        Configuration.instance.getPathsToBackUp().contains(folder.path);
+    return GestureDetector(
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 2),
+        child: SizedBox(
+          height: 140,
+          width: 120,
+          child: Column(
+            children: <Widget>[
+              ClipRRect(
+                borderRadius: BorderRadius.circular(4),
+                child: SizedBox(
+                  height: 120,
+                  width: 120,
+                  child: Hero(
+                    tag:
+                        "device_folder:" + folder.path + folder.thumbnail.tag(),
+                    child: Stack(
+                      children: [
+                        ThumbnailWidget(
+                          folder.thumbnail,
+                          shouldShowSyncStatus: false,
+                          key: Key(
+                            "device_folder:" +
+                                folder.path +
+                                folder.thumbnail.tag(),
+                          ),
+                        ),
+                        isBackedUp ? Container() : kUnsyncedIconOverlay,
+                      ],
+                    ),
+                  ),
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsets.only(top: 10),
+                child: Text(
+                  folder.name,
+                  style: Theme.of(context)
+                      .textTheme
+                      .subtitle1
+                      .copyWith(fontSize: 12),
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(context, DeviceFolderPage(folder));
+      },
+    );
+  }
+}

+ 39 - 0
lib/ui/collections/device_folders_grid_view_widget.dart

@@ -0,0 +1,39 @@
+import 'package:flutter/material.dart';
+import 'package:photos/models/device_folder.dart';
+import 'package:photos/ui/collections/device_folder_icon_widget.dart';
+import 'package:photos/ui/viewer/gallery/empte_state.dart';
+
+class DeviceFoldersGridViewWidget extends StatelessWidget {
+  final List<DeviceFolder> folders;
+
+  const DeviceFoldersGridViewWidget(
+    this.folders, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      child: SizedBox(
+        height: 170,
+        child: Align(
+          alignment: Alignment.centerLeft,
+          child: folders.isEmpty
+              ? const EmptyState()
+              : ListView.builder(
+                  shrinkWrap: true,
+                  scrollDirection: Axis.horizontal,
+                  padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
+                  physics: const ScrollPhysics(),
+                  // to disable GridView's scrolling
+                  itemBuilder: (context, index) {
+                    return DeviceFolderIcon(folders[index]);
+                  },
+                  itemCount: folders.length,
+                ),
+        ),
+      ),
+    );
+  }
+}

+ 47 - 0
lib/ui/collections/ente_section_title.dart

@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+
+class EnteSectionTitle extends StatelessWidget {
+  final double opacity;
+
+  const EnteSectionTitle({
+    this.opacity = 0.8,
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
+      child: Column(
+        children: [
+          Align(
+            alignment: Alignment.centerLeft,
+            child: RichText(
+              text: TextSpan(
+                children: [
+                  TextSpan(
+                    text: "On ",
+                    style: Theme.of(context)
+                        .textTheme
+                        .headline6
+                        .copyWith(fontSize: 22),
+                  ),
+                  TextSpan(
+                    text: "ente",
+                    style: TextStyle(
+                      fontWeight: FontWeight.bold,
+                      fontFamily: 'Montserrat',
+                      fontSize: 22,
+                      color: Theme.of(context).colorScheme.defaultTextColor,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 102 - 0
lib/ui/collections/hidden_collections_button_widget.dart

@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/ui/viewer/gallery/archive_page.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class HiddenCollectionsButtonWidget extends StatelessWidget {
+  final TextStyle textStyle;
+
+  const HiddenCollectionsButtonWidget(
+    this.textStyle, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return OutlinedButton(
+      style: OutlinedButton.styleFrom(
+        backgroundColor: Theme.of(context).backgroundColor,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(8),
+        ),
+        padding: const EdgeInsets.all(0),
+        side: BorderSide(
+          width: 0.5,
+          color: Theme.of(context).iconTheme.color.withOpacity(0.24),
+        ),
+      ),
+      child: SizedBox(
+        height: 48,
+        width: double.infinity,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Row(
+                children: [
+                  Icon(
+                    Icons.visibility_off,
+                    color: Theme.of(context).iconTheme.color,
+                  ),
+                  const Padding(padding: EdgeInsets.all(6)),
+                  FutureBuilder<int>(
+                    future: FilesDB.instance.fileCountWithVisibility(
+                      kVisibilityArchive,
+                      Configuration.instance.getUserID(),
+                    ),
+                    builder: (context, snapshot) {
+                      if (snapshot.hasData && snapshot.data > 0) {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Hidden",
+                                style: Theme.of(context).textTheme.subtitle1,
+                              ),
+                              const TextSpan(text: "  \u2022  "),
+                              TextSpan(
+                                text: snapshot.data.toString(),
+                              ),
+                              //need to query in db and bring this value
+                            ],
+                          ),
+                        );
+                      } else {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Hidden",
+                                style: Theme.of(context).textTheme.subtitle1,
+                              ),
+                              //need to query in db and bring this value
+                            ],
+                          ),
+                        );
+                      }
+                    },
+                  ),
+                ],
+              ),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).iconTheme.color,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onPressed: () async {
+        routeToPage(
+          context,
+          ArchivePage(),
+        );
+      },
+    );
+  }
+}

+ 50 - 0
lib/ui/collections/remote_collections_grid_view_widget.dart

@@ -0,0 +1,50 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:photos/models/collection_items.dart';
+import 'package:photos/ui/collections/collection_item_widget.dart';
+import 'package:photos/ui/collections/create_new_album_widget.dart';
+
+class RemoteCollectionsGridViewWidget extends StatelessWidget {
+  final List<CollectionWithThumbnail> collections;
+
+  const RemoteCollectionsGridViewWidget(
+    this.collections, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    const double horizontalPaddingOfGridRow = 16;
+    const double crossAxisSpacingOfGrid = 9;
+    Size size = MediaQuery.of(context).size;
+    int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
+    final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
+        horizontalPaddingOfGridRow -
+        ((crossAxisSpacingOfGrid / 2) * (albumsCountInOneRow - 1));
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16),
+      child: GridView.builder(
+        shrinkWrap: true,
+        physics: const ScrollPhysics(),
+        // to disable GridView's scrolling
+        itemBuilder: (context, index) {
+          if (index < collections.length) {
+            return CollectionItem(collections[index]);
+          } else {
+            return const CreateNewAlbumWidget();
+          }
+        },
+        itemCount: collections.length + 1,
+        // To include the + button
+        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+          crossAxisCount: albumsCountInOneRow,
+          mainAxisSpacing: 12,
+          crossAxisSpacing: crossAxisSpacingOfGrid,
+          childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 24),
+        ), //24 is height of album title
+      ),
+    );
+  }
+}

+ 33 - 0
lib/ui/collections/section_title.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class SectionTitle extends StatelessWidget {
+  final String title;
+  final Alignment alignment;
+  final double opacity;
+
+  const SectionTitle(
+    this.title, {
+    this.opacity = 0.8,
+    Key key,
+    this.alignment = Alignment.centerLeft,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
+      child: Column(
+        children: [
+          Align(
+            alignment: alignment,
+            child: Text(
+              title,
+              style:
+                  Theme.of(context).textTheme.headline6.copyWith(fontSize: 22),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 97 - 0
lib/ui/collections/trash_button_widget.dart

@@ -0,0 +1,97 @@
+import 'package:flutter/material.dart';
+import 'package:photos/db/trash_db.dart';
+import 'package:photos/ui/viewer/gallery/trash_page.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class TrashButtonWidget extends StatelessWidget {
+  const TrashButtonWidget(
+    this.textStyle, {
+    Key key,
+  }) : super(key: key);
+
+  final TextStyle textStyle;
+
+  @override
+  Widget build(BuildContext context) {
+    return OutlinedButton(
+      style: OutlinedButton.styleFrom(
+        backgroundColor: Theme.of(context).backgroundColor,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(8),
+        ),
+        padding: const EdgeInsets.all(0),
+        side: BorderSide(
+          width: 0.5,
+          color: Theme.of(context).iconTheme.color.withOpacity(0.24),
+        ),
+      ),
+      child: SizedBox(
+        height: 48,
+        width: double.infinity,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Row(
+                children: [
+                  Icon(
+                    Icons.delete,
+                    color: Theme.of(context).iconTheme.color,
+                  ),
+                  const Padding(padding: EdgeInsets.all(6)),
+                  FutureBuilder<int>(
+                    future: TrashDB.instance.count(),
+                    builder: (context, snapshot) {
+                      if (snapshot.hasData && snapshot.data > 0) {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Trash",
+                                style: Theme.of(context).textTheme.subtitle1,
+                              ),
+                              const TextSpan(text: "  \u2022  "),
+                              TextSpan(
+                                text: snapshot.data.toString(),
+                              ),
+                              //need to query in db and bring this value
+                            ],
+                          ),
+                        );
+                      } else {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Trash",
+                                style: Theme.of(context).textTheme.subtitle1,
+                              ),
+                              //need to query in db and bring this value
+                            ],
+                          ),
+                        );
+                      }
+                    },
+                  ),
+                ],
+              ),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).iconTheme.color,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onPressed: () async {
+        routeToPage(
+          context,
+          TrashPage(),
+        );
+      },
+    );
+  }
+}

+ 11 - 533
lib/ui/collections_gallery_widget.dart

@@ -1,35 +1,29 @@
 import 'dart:async';
 import 'dart:io';
-import 'dart:math';
 
 import 'package:flutter/material.dart';
-import 'package:fluttertoast/fluttertoast.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
-import 'package:photos/db/trash_db.dart';
-import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/backup_folders_updated_event.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
-import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/device_folder.dart';
-import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/collections/device_folders_grid_view_widget.dart';
+import 'package:photos/ui/collections/ente_section_title.dart';
+import 'package:photos/ui/collections/hidden_collections_button_widget.dart';
+import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
+import 'package:photos/ui/collections/section_title.dart';
+import 'package:photos/ui/collections/trash_button_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
-import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
-import 'package:photos/ui/viewer/gallery/archive_page.dart';
-import 'package:photos/ui/viewer/gallery/collection_page.dart';
 import 'package:photos/ui/viewer/gallery/device_all_page.dart';
-import 'package:photos/ui/viewer/gallery/device_folder_page.dart';
 import 'package:photos/ui/viewer/gallery/empte_state.dart';
-import 'package:photos/ui/viewer/gallery/trash_page.dart';
 import 'package:photos/utils/local_settings.dart';
 import 'package:photos/utils/navigation_util.dart';
-import 'package:photos/utils/toast_util.dart';
 
 class CollectionsGalleryWidget extends StatefulWidget {
   const CollectionsGalleryWidget({Key key}) : super(key: key);
@@ -133,19 +127,13 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
   }
 
   Widget _getCollectionsGalleryWidget(CollectionItems items) {
-    const double horizontalPaddingOfGridRow = 16;
-    const double crossAxisSpacingOfGrid = 9;
     final TextStyle trashAndHiddenTextStyle = Theme.of(context)
         .textTheme
         .subtitle1
         .copyWith(
           color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.5),
         );
-    Size size = MediaQuery.of(context).size;
-    int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
-    final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
-        horizontalPaddingOfGridRow -
-        ((crossAxisSpacingOfGrid / 2) * (albumsCountInOneRow - 1));
+
     return SingleChildScrollView(
       child: Container(
         margin: const EdgeInsets.only(bottom: 50),
@@ -174,28 +162,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
                     padding: EdgeInsets.all(22),
                     child: EmptyState(),
                   )
-                : Padding(
-                    padding: const EdgeInsets.symmetric(horizontal: 8),
-                    child: SizedBox(
-                      height: 170,
-                      child: Align(
-                        alignment: Alignment.centerLeft,
-                        child: items.folders.isEmpty
-                            ? const EmptyState()
-                            : ListView.builder(
-                                shrinkWrap: true,
-                                scrollDirection: Axis.horizontal,
-                                padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
-                                physics: const ScrollPhysics(),
-                                // to disable GridView's scrolling
-                                itemBuilder: (context, index) {
-                                  return DeviceFolderIcon(items.folders[index]);
-                                },
-                                itemCount: items.folders.length,
-                              ),
-                      ),
-                    ),
-                  ),
+                : DeviceFoldersGridViewWidget(items.folders),
             const Padding(padding: EdgeInsets.all(4)),
             const Divider(),
             Row(
@@ -208,30 +175,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
             ),
             const SizedBox(height: 12),
             Configuration.instance.hasConfiguredAccount()
-                ? Padding(
-                    padding: const EdgeInsets.symmetric(horizontal: 16),
-                    child: GridView.builder(
-                      shrinkWrap: true,
-                      physics: const ScrollPhysics(),
-                      // to disable GridView's scrolling
-                      itemBuilder: (context, index) {
-                        return _buildCollection(
-                          context,
-                          items.collections,
-                          index,
-                        );
-                      },
-                      itemCount: items.collections.length + 1,
-                      // To include the + button
-                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-                        crossAxisCount: albumsCountInOneRow,
-                        mainAxisSpacing: 12,
-                        crossAxisSpacing: crossAxisSpacingOfGrid,
-                        childAspectRatio:
-                            sideOfThumbnail / (sideOfThumbnail + 24),
-                      ), //24 is height of album title
-                    ),
-                  )
+                ? RemoteCollectionsGridViewWidget(items.collections)
                 : const EmptyState(),
             const SizedBox(height: 10),
             const Divider(),
@@ -240,181 +184,9 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Column(
                 children: [
-                  OutlinedButton(
-                    style: OutlinedButton.styleFrom(
-                      backgroundColor: Theme.of(context).backgroundColor,
-                      shape: RoundedRectangleBorder(
-                        borderRadius: BorderRadius.circular(8),
-                      ),
-                      padding: const EdgeInsets.all(0),
-                      side: BorderSide(
-                        width: 0.5,
-                        color:
-                            Theme.of(context).iconTheme.color.withOpacity(0.24),
-                      ),
-                    ),
-                    child: SizedBox(
-                      height: 48,
-                      width: double.infinity,
-                      child: Padding(
-                        padding: const EdgeInsets.symmetric(horizontal: 16),
-                        child: Row(
-                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                          children: [
-                            Row(
-                              children: [
-                                Icon(
-                                  Icons.delete,
-                                  color: Theme.of(context).iconTheme.color,
-                                ),
-                                const Padding(padding: EdgeInsets.all(6)),
-                                FutureBuilder<int>(
-                                  future: TrashDB.instance.count(),
-                                  builder: (context, snapshot) {
-                                    if (snapshot.hasData && snapshot.data > 0) {
-                                      return RichText(
-                                        text: TextSpan(
-                                          style: trashAndHiddenTextStyle,
-                                          children: [
-                                            TextSpan(
-                                              text: "Trash",
-                                              style: Theme.of(context)
-                                                  .textTheme
-                                                  .subtitle1,
-                                            ),
-                                            const TextSpan(text: "  \u2022  "),
-                                            TextSpan(
-                                              text: snapshot.data.toString(),
-                                            ),
-                                            //need to query in db and bring this value
-                                          ],
-                                        ),
-                                      );
-                                    } else {
-                                      return RichText(
-                                        text: TextSpan(
-                                          style: trashAndHiddenTextStyle,
-                                          children: [
-                                            TextSpan(
-                                              text: "Trash",
-                                              style: Theme.of(context)
-                                                  .textTheme
-                                                  .subtitle1,
-                                            ),
-                                            //need to query in db and bring this value
-                                          ],
-                                        ),
-                                      );
-                                    }
-                                  },
-                                ),
-                              ],
-                            ),
-                            Icon(
-                              Icons.chevron_right,
-                              color: Theme.of(context).iconTheme.color,
-                            ),
-                          ],
-                        ),
-                      ),
-                    ),
-                    onPressed: () async {
-                      routeToPage(
-                        context,
-                        TrashPage(),
-                      );
-                    },
-                  ),
+                  TrashButtonWidget(trashAndHiddenTextStyle),
                   const SizedBox(height: 12),
-                  OutlinedButton(
-                    style: OutlinedButton.styleFrom(
-                      backgroundColor: Theme.of(context).backgroundColor,
-                      shape: RoundedRectangleBorder(
-                        borderRadius: BorderRadius.circular(8),
-                      ),
-                      padding: const EdgeInsets.all(0),
-                      side: BorderSide(
-                        width: 0.5,
-                        color:
-                            Theme.of(context).iconTheme.color.withOpacity(0.24),
-                      ),
-                    ),
-                    child: SizedBox(
-                      height: 48,
-                      width: double.infinity,
-                      child: Padding(
-                        padding: const EdgeInsets.symmetric(horizontal: 16),
-                        child: Row(
-                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                          children: [
-                            Row(
-                              children: [
-                                Icon(
-                                  Icons.visibility_off,
-                                  color: Theme.of(context).iconTheme.color,
-                                ),
-                                const Padding(padding: EdgeInsets.all(6)),
-                                FutureBuilder<int>(
-                                  future:
-                                      FilesDB.instance.fileCountWithVisibility(
-                                    kVisibilityArchive,
-                                    Configuration.instance.getUserID(),
-                                  ),
-                                  builder: (context, snapshot) {
-                                    if (snapshot.hasData && snapshot.data > 0) {
-                                      return RichText(
-                                        text: TextSpan(
-                                          style: trashAndHiddenTextStyle,
-                                          children: [
-                                            TextSpan(
-                                              text: "Hidden",
-                                              style: Theme.of(context)
-                                                  .textTheme
-                                                  .subtitle1,
-                                            ),
-                                            const TextSpan(text: "  \u2022  "),
-                                            TextSpan(
-                                              text: snapshot.data.toString(),
-                                            ),
-                                            //need to query in db and bring this value
-                                          ],
-                                        ),
-                                      );
-                                    } else {
-                                      return RichText(
-                                        text: TextSpan(
-                                          style: trashAndHiddenTextStyle,
-                                          children: [
-                                            TextSpan(
-                                              text: "Hidden",
-                                              style: Theme.of(context)
-                                                  .textTheme
-                                                  .subtitle1,
-                                            ),
-                                            //need to query in db and bring this value
-                                          ],
-                                        ),
-                                      );
-                                    }
-                                  },
-                                ),
-                              ],
-                            ),
-                            Icon(
-                              Icons.chevron_right,
-                              color: Theme.of(context).iconTheme.color,
-                            ),
-                          ],
-                        ),
-                      ),
-                    ),
-                    onPressed: () async {
-                      routeToPage(
-                        context,
-                        ArchivePage(),
-                      );
-                    },
-                  ),
+                  HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
                 ],
               ),
             ),
@@ -493,48 +265,6 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
     );
   }
 
-  Widget _buildCollection(
-    BuildContext context,
-    List<CollectionWithThumbnail> collections,
-    int index,
-  ) {
-    if (index < collections.length) {
-      final c = collections[index];
-      return CollectionItem(c);
-    } else {
-      return InkWell(
-        child: Container(
-          margin: const EdgeInsets.fromLTRB(30, 30, 30, 54),
-          decoration: BoxDecoration(
-            color: Theme.of(context).backgroundColor,
-            boxShadow: [
-              BoxShadow(
-                blurRadius: 2,
-                spreadRadius: 0,
-                offset: const Offset(0, 0),
-                color: Theme.of(context).iconTheme.color.withOpacity(0.3),
-              )
-            ],
-            borderRadius: BorderRadius.circular(4),
-          ),
-          child: Icon(
-            Icons.add,
-            color: Theme.of(context).iconTheme.color.withOpacity(0.25),
-          ),
-        ),
-        onTap: () async {
-          await showToast(
-            context,
-            "long press to select photos and click + to create an album",
-            toastLength: Toast.LENGTH_LONG,
-          );
-          Bus.instance
-              .fire(TabChangedEvent(0, TabChangedEventSource.collectionsPage));
-        },
-      );
-    }
-  }
-
   @override
   void dispose() {
     _localFilesSubscription.cancel();
@@ -547,255 +277,3 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
   @override
   bool get wantKeepAlive => true;
 }
-
-class DeviceFolderIcon extends StatelessWidget {
-  const DeviceFolderIcon(
-    this.folder, {
-    Key key,
-  }) : super(key: key);
-
-  static final kUnsyncedIconOverlay = Container(
-    decoration: BoxDecoration(
-      gradient: LinearGradient(
-        begin: Alignment.topCenter,
-        end: Alignment.bottomCenter,
-        colors: [
-          Colors.transparent,
-          Colors.black.withOpacity(0.6),
-        ],
-        stops: const [0.7, 1],
-      ),
-    ),
-    child: Align(
-      alignment: Alignment.bottomRight,
-      child: Padding(
-        padding: const EdgeInsets.only(right: 8, bottom: 8),
-        child: Icon(
-          Icons.cloud_off_outlined,
-          size: 18,
-          color: Colors.white.withOpacity(0.9),
-        ),
-      ),
-    ),
-  );
-
-  final DeviceFolder folder;
-
-  @override
-  Widget build(BuildContext context) {
-    final isBackedUp =
-        Configuration.instance.getPathsToBackUp().contains(folder.path);
-    return GestureDetector(
-      child: Padding(
-        padding: const EdgeInsets.symmetric(horizontal: 2),
-        child: SizedBox(
-          height: 140,
-          width: 120,
-          child: Column(
-            children: <Widget>[
-              ClipRRect(
-                borderRadius: BorderRadius.circular(4),
-                child: SizedBox(
-                  height: 120,
-                  width: 120,
-                  child: Hero(
-                    tag:
-                        "device_folder:" + folder.path + folder.thumbnail.tag(),
-                    child: Stack(
-                      children: [
-                        ThumbnailWidget(
-                          folder.thumbnail,
-                          shouldShowSyncStatus: false,
-                          key: Key(
-                            "device_folder:" +
-                                folder.path +
-                                folder.thumbnail.tag(),
-                          ),
-                        ),
-                        isBackedUp ? Container() : kUnsyncedIconOverlay,
-                      ],
-                    ),
-                  ),
-                ),
-              ),
-              Padding(
-                padding: const EdgeInsets.only(top: 10),
-                child: Text(
-                  folder.name,
-                  style: Theme.of(context)
-                      .textTheme
-                      .subtitle1
-                      .copyWith(fontSize: 12),
-                  overflow: TextOverflow.ellipsis,
-                ),
-              ),
-            ],
-          ),
-        ),
-      ),
-      onTap: () {
-        routeToPage(context, DeviceFolderPage(folder));
-      },
-    );
-  }
-}
-
-class CollectionItem extends StatelessWidget {
-  CollectionItem(
-    this.c, {
-    Key key,
-  }) : super(key: Key(c.collection.id.toString()));
-
-  final CollectionWithThumbnail c;
-
-  @override
-  Widget build(BuildContext context) {
-    const double horizontalPaddingOfGridRow = 16;
-    const double crossAxisSpacingOfGrid = 9;
-    Size size = MediaQuery.of(context).size;
-    int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
-    double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
-        (albumsCountInOneRow - 1) * crossAxisSpacingOfGrid;
-    TextStyle albumTitleTextStyle =
-        Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14);
-    final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
-        (totalWhiteSpaceOfRow / albumsCountInOneRow);
-    return GestureDetector(
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: <Widget>[
-          ClipRRect(
-            borderRadius: BorderRadius.circular(4),
-            child: SizedBox(
-              height: sideOfThumbnail,
-              width: sideOfThumbnail,
-              child: Hero(
-                tag: "collection" + c.thumbnail.tag(),
-                child: ThumbnailWidget(
-                  c.thumbnail,
-                  shouldShowArchiveStatus: c.collection.isArchived(),
-                  key: Key(
-                    "collection" + c.thumbnail.tag(),
-                  ),
-                ),
-              ),
-            ),
-          ),
-          const SizedBox(height: 4),
-          Row(
-            children: [
-              Container(
-                constraints: BoxConstraints(maxWidth: sideOfThumbnail - 40),
-                child: Text(
-                  c.collection.name,
-                  style: albumTitleTextStyle,
-                  overflow: TextOverflow.ellipsis,
-                ),
-              ),
-              FutureBuilder<int>(
-                future: FilesDB.instance.collectionFileCount(c.collection.id),
-                builder: (context, snapshot) {
-                  if (snapshot.hasData && snapshot.data > 0) {
-                    return RichText(
-                      text: TextSpan(
-                        style: albumTitleTextStyle.copyWith(
-                          color: albumTitleTextStyle.color.withOpacity(0.5),
-                        ),
-                        children: [
-                          const TextSpan(text: "  \u2022  "),
-                          TextSpan(text: snapshot.data.toString()),
-                        ],
-                      ),
-                    );
-                  } else {
-                    return const SizedBox.shrink();
-                  }
-                },
-              ),
-            ],
-          ),
-        ],
-      ),
-      onTap: () {
-        routeToPage(context, CollectionPage(c));
-      },
-    );
-  }
-}
-
-class SectionTitle extends StatelessWidget {
-  final String title;
-  final Alignment alignment;
-  final double opacity;
-
-  const SectionTitle(
-    this.title, {
-    this.opacity = 0.8,
-    Key key,
-    this.alignment = Alignment.centerLeft,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
-      child: Column(
-        children: [
-          Align(
-            alignment: alignment,
-            child: Text(
-              title,
-              style:
-                  Theme.of(context).textTheme.headline6.copyWith(fontSize: 22),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-}
-
-class EnteSectionTitle extends StatelessWidget {
-  final double opacity;
-
-  const EnteSectionTitle({
-    this.opacity = 0.8,
-    Key key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
-      child: Column(
-        children: [
-          Align(
-            alignment: Alignment.centerLeft,
-            child: RichText(
-              text: TextSpan(
-                children: [
-                  TextSpan(
-                    text: "On ",
-                    style: Theme.of(context)
-                        .textTheme
-                        .headline6
-                        .copyWith(fontSize: 22),
-                  ),
-                  TextSpan(
-                    text: "ente",
-                    style: TextStyle(
-                      fontWeight: FontWeight.bold,
-                      fontFamily: 'Montserrat',
-                      fontSize: 22,
-                      color: Theme.of(context).colorScheme.defaultTextColor,
-                    ),
-                  ),
-                ],
-              ),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-}

+ 1 - 0
lib/ui/home_widget.dart

@@ -260,6 +260,7 @@ class _HomeWidgetState extends State<HomeWidget> {
           child: Container(),
         ),
         body: _getBody(),
+        resizeToAvoidBottomInset: false,
       ),
       onWillPop: () async {
         if (_selectedTabIndex == 0) {

+ 15 - 13
lib/ui/huge_listview/draggable_scrollbar.dart

@@ -124,20 +124,22 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
     }
   }
 
-  Widget buildThumb() => Container(
-        alignment: Alignment.topRight,
-        margin: EdgeInsets.only(top: thumbOffset),
+  Widget buildThumb() => Padding(
         padding: widget.padding,
-        child: ScrollBarThumb(
-          widget.backgroundColor,
-          widget.drawColor,
-          widget.heightScrollThumb,
-          widget.labelTextBuilder.call(currentFirstIndex),
-          _labelAnimation,
-          _thumbAnimation,
-          onDragStart,
-          onDragUpdate,
-          onDragEnd,
+        child: Container(
+          alignment: Alignment.topRight,
+          margin: EdgeInsets.only(top: thumbOffset),
+          child: ScrollBarThumb(
+            widget.backgroundColor,
+            widget.drawColor,
+            widget.heightScrollThumb,
+            widget.labelTextBuilder.call(currentFirstIndex),
+            _labelAnimation,
+            _thumbAnimation,
+            onDragStart,
+            onDragUpdate,
+            onDragEnd,
+          ),
         ),
       );
 

+ 4 - 0
lib/ui/huge_listview/huge_listview.dart

@@ -54,6 +54,8 @@ class HugeListView<T> extends StatefulWidget {
 
   final bool isDraggableScrollbarEnabled;
 
+  final EdgeInsetsGeometry thumbPadding;
+
   const HugeListView({
     Key key,
     this.controller,
@@ -69,6 +71,7 @@ class HugeListView<T> extends StatefulWidget {
     this.thumbDrawColor = Colors.yellow, //Colors.grey,
     this.thumbHeight = 48.0,
     this.isDraggableScrollbarEnabled = true,
+    this.thumbPadding,
   }) : super(key: key);
 
   @override
@@ -135,6 +138,7 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
           heightScrollThumb: widget.thumbHeight,
           currentFirstIndex: _currentFirst(),
           isEnabled: widget.isDraggableScrollbarEnabled,
+          padding: widget.thumbPadding,
           child: ScrollablePositionedList.builder(
             itemScrollController: widget.controller,
             itemPositionsListener: listener,

+ 1 - 1
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -145,7 +145,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
   @override
   Widget build(BuildContext context) {
     if (_files.isEmpty) {
-      return Container();
+      return const SizedBox.shrink();
     }
     return Padding(
       padding: const EdgeInsets.only(bottom: 12),

+ 0 - 4
lib/ui/nav_bar.dart

@@ -334,7 +334,6 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
         highlightColor: widget.hoverColor,
         splashColor: widget.rippleColor,
         borderRadius: BorderRadius.circular(100),
-        // behavior: HitTestBehavior.opaque,
         onTap: () {
           widget.onPressed();
         },
@@ -342,11 +341,8 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
           padding: widget.margin,
           child: AnimatedContainer(
             curve: Curves.easeOut,
-            // padding: EdgeInsets.symmetric(horizontal: 5),
             padding: widget.padding,
-            // curve: Curves.easeOutQuad,
             duration: widget.duration,
-            // curve: !_expanded ? widget.curve : widget.curve.flipped,
             decoration: BoxDecoration(
               boxShadow: widget.shadow,
               border: widget.active

+ 6 - 6
lib/ui/payment/subscription_page.dart

@@ -72,16 +72,16 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
               purchase.verificationData.serverVerificationData,
             );
             await InAppPurchaseConnection.instance.completePurchase(purchase);
-            String text = "thank you for subscribing!";
+            String text = "Thank you for subscribing!";
             if (!widget.isOnboarding) {
               final isUpgrade = _hasActiveSubscription &&
                   newSubscription.storage > _currentSubscription.storage;
               final isDowngrade = _hasActiveSubscription &&
                   newSubscription.storage < _currentSubscription.storage;
               if (isUpgrade) {
-                text = "your plan was successfully upgraded";
+                text = "Your plan was successfully upgraded";
               } else if (isDowngrade) {
-                text = "your plan was successfully downgraded";
+                text = "Your plan was successfully downgraded";
               }
             }
             showToast(context, text);
@@ -98,8 +98,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
             await _dialog.hide();
             showErrorDialog(
               context,
-              "payment failed",
-              "please talk to " +
+              "Payment failed",
+              "Please talk to " +
                   (Platform.isAndroid ? "PlayStore" : "AppStore") +
                   " support if you were charged",
             );
@@ -458,7 +458,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     if (_userDetails.subscription.productID == kFreeProductID) {
       await showErrorDialog(
         context,
-        "Now you can share your storage plan with your family members!",
+        "Share your storage plan with your family members!",
         "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
       );
       return;

+ 1 - 1
lib/ui/shared_collections_gallery.dart

@@ -15,7 +15,7 @@ import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/services/collections_service.dart';
-import 'package:photos/ui/collections_gallery_widget.dart';
+import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';

+ 9 - 11
lib/ui/status_bar_widget.dart

@@ -60,7 +60,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
               AnimatedOpacity(
                 opacity: _showStatus ? 0 : 1,
                 duration: const Duration(milliseconds: 1000),
-                child: const StatusBarBrandingWidget(),
+                child: const TopBarWidget(),
               ),
               AnimatedOpacity(
                 opacity: _showStatus ? 1 : 0,
@@ -206,12 +206,13 @@ class RefreshIndicatorWidget extends StatelessWidget {
   }
 }
 
-class StatusBarBrandingWidget extends StatelessWidget {
-  const StatusBarBrandingWidget({Key key}) : super(key: key);
+class TopBarWidget extends StatelessWidget {
+  const TopBarWidget({Key key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return Stack(
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: [
         Container(
           height: kContainerHeight,
@@ -230,14 +231,11 @@ class StatusBarBrandingWidget extends StatelessWidget {
           ),
         ),
         FeatureFlagService.instance.enableSearchFeature()
-            ? SizedBox(
-                width: MediaQuery.of(context).size.width,
-                child: const Align(
-                  alignment: Alignment.centerRight,
-                  child: SearchIconWidget(),
-                ),
+            ? const SizedBox(
+                height: kContainerHeight,
+                child: SearchIconWidget(),
               )
-            : const SizedBox.shrink(),
+            : const SizedBox.shrink()
       ],
     );
   }

+ 3 - 37
lib/ui/viewer/file/fading_app_bar.dart

@@ -3,14 +3,12 @@ import 'dart:io' as io;
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
 import 'package:like_button/like_button.dart';
 import 'package:logging/logging.dart';
-import 'package:path/path.dart' as filePath;
+import 'package:path/path.dart' as file_path;
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
-import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
@@ -24,7 +22,6 @@ import 'package:photos/ui/viewer/file/custom_app_bar.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';
-import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
 class FadingAppBar extends StatefulWidget implements PreferredSizeWidget {
@@ -233,37 +230,6 @@ class FadingAppBarState extends State<FadingAppBar> {
     );
   }
 
-  void _showDateTimePicker(File file) async {
-    final dateResult = await DatePicker.showDatePicker(
-      context,
-      minTime: DateTime(1800, 1, 1),
-      maxTime: DateTime.now(),
-      currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateResult == null) {
-      return;
-    }
-    final dateWithTimeResult = await DatePicker.showTime12hPicker(
-      context,
-      showTitleActions: true,
-      currentTime: dateResult,
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateWithTimeResult != null) {
-      if (await editTime(
-        context,
-        List.of([widget.file]),
-        dateWithTimeResult.microsecondsSinceEpoch,
-      )) {
-        widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch;
-        setState(() {});
-      }
-    }
-  }
-
   void _showDeleteSheet(File file) {
     final List<Widget> actions = [];
     if (file.uploadedFileID == null || file.localID == null) {
@@ -351,8 +317,8 @@ class FadingAppBarState extends State<FadingAppBar> {
       if (liveVideo == null) {
         _logger.warning("Failed to find live video" + file.tag());
       } else {
-        final videoTitle = filePath.basenameWithoutExtension(file.title) +
-            filePath.extension(liveVideo.path);
+        final videoTitle = file_path.basenameWithoutExtension(file.title) +
+            file_path.extension(liveVideo.path);
         final savedAsset = (await PhotoManager.editor.saveVideo(
           liveVideo,
           title: videoTitle,

+ 1 - 1
lib/ui/viewer/file/zoomable_live_image.dart

@@ -166,7 +166,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
   void _showLivePhotoToast() async {
     var preferences = await SharedPreferences.getInstance();
     int promptTillNow = preferences.getInt(kLivePhotoToastCounterKey) ?? 0;
-    if (promptTillNow < kMaxLivePhotoToastCount) {
+    if (promptTillNow < kMaxLivePhotoToastCount && mounted) {
       showToast(context, "Press and hold to play video");
       preferences.setInt(kLivePhotoToastCounterKey, promptTillNow + 1);
     }

+ 0 - 1
lib/ui/viewer/gallery/archive_page.dart

@@ -58,7 +58,6 @@ class ArchivePage extends StatelessWidget {
       tagPrefix: tagPrefix,
       selectedFiles: _selectedFiles,
       initialFiles: null,
-      footer: const SizedBox(height: 120),
     );
     return Scaffold(
       appBar: PreferredSize(

+ 0 - 1
lib/ui/viewer/gallery/collection_page.dart

@@ -48,7 +48,6 @@ class CollectionPage extends StatelessWidget {
       initialFiles: initialFiles,
       smallerTodayFont: true,
       albumName: c.collection.name,
-      footer: const SizedBox(height: 120),
     );
     return Scaffold(
       appBar: PreferredSize(

+ 0 - 1
lib/ui/viewer/gallery/device_all_page.dart

@@ -34,7 +34,6 @@ class DeviceAllPage extends StatelessWidget {
       tagPrefix: "device_all",
       selectedFiles: _selectedFiles,
       initialFiles: null,
-      footer: const SizedBox(height: 120),
     );
     return Scaffold(
       appBar: PreferredSize(

+ 0 - 1
lib/ui/viewer/gallery/device_folder_page.dart

@@ -42,7 +42,6 @@ class DeviceFolderPage extends StatelessWidget {
           ? _getHeaderWidget()
           : Container(),
       initialFiles: [folder.thumbnail],
-      footer: const SizedBox(height: 120),
     );
     return Scaffold(
       appBar: PreferredSize(

+ 4 - 1
lib/ui/viewer/gallery/gallery.dart

@@ -46,7 +46,7 @@ class Gallery extends StatefulWidget {
     this.forceReloadEvents,
     this.removalEventTypes = const {},
     this.header,
-    this.footer,
+    this.footer = const SizedBox(height: 120),
     this.smallerTodayFont = false,
     this.albumName = '',
     Key key,
@@ -234,6 +234,9 @@ class _GalleryState extends State<Gallery> {
       thumbBackgroundColor:
           Theme.of(context).colorScheme.galleryThumbBackgroundColor,
       thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor,
+      thumbPadding: widget.header != null
+          ? const EdgeInsets.only(top: 60)
+          : const EdgeInsets.all(0),
       firstShown: (int firstIndex) {
         Bus.instance
             .fire(GalleryIndexUpdatedEvent(widget.tagPrefix, firstIndex));

+ 2 - 1
lib/ui/viewer/gallery/gallery_overlay_widget.dart

@@ -321,7 +321,8 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     if (widget.type == GalleryType.homepage ||
         widget.type == GalleryType.archive ||
         widget.type == GalleryType.localFolder ||
-        widget.type == GalleryType.localAll) {
+        widget.type == GalleryType.localAll ||
+        widget.type == GalleryType.searchResults) {
       actions.add(
         Tooltip(
           message: "Delete",

+ 0 - 1
lib/ui/viewer/gallery/trash_page.dart

@@ -74,7 +74,6 @@ class _TrashPageState extends State<TrashPage> {
       selectedFiles: widget._selectedFiles,
       header: _headerWidget(),
       initialFiles: null,
-      footer: const SizedBox(height: 120),
     );
 
     return Scaffold(

+ 0 - 75
lib/ui/viewer/search/collection_result_widget.dart

@@ -1,75 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:photos/db/files_db.dart';
-import 'package:photos/ente_theme_data.dart';
-import 'package:photos/models/collection_items.dart';
-import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
-import 'package:photos/ui/viewer/gallery/collection_page.dart';
-import 'package:photos/utils/navigation_util.dart';
-
-class CollectionResultWidget extends StatelessWidget {
-  final CollectionWithThumbnail c;
-
-  const CollectionResultWidget(this.c, {Key key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return GestureDetector(
-      child: Padding(
-        padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                const Text(
-                  'Album',
-                  style: TextStyle(fontSize: 12),
-                ),
-                const SizedBox(height: 8),
-                Text(
-                  c.collection.name,
-                  style: const TextStyle(fontSize: 18),
-                ),
-                FutureBuilder<int>(
-                  future: FilesDB.instance.collectionFileCount(
-                    c.collection.id,
-                  ),
-                  builder: (context, snapshot) {
-                    if (snapshot.hasData && snapshot.data > 0) {
-                      int noOfMemories = snapshot.data;
-                      return RichText(
-                        text: TextSpan(
-                          style: TextStyle(
-                            color:
-                                Theme.of(context).colorScheme.defaultTextColor,
-                          ),
-                          children: [
-                            TextSpan(text: noOfMemories.toString()),
-                            TextSpan(
-                              text: noOfMemories != 1 ? ' memories' : ' memory',
-                            ),
-                          ],
-                        ),
-                      );
-                    } else {
-                      return const SizedBox.shrink();
-                    }
-                  },
-                ),
-              ],
-            ),
-            SizedBox(
-              height: 50,
-              width: 50,
-              child: ThumbnailWidget(c.thumbnail),
-            )
-          ],
-        ),
-      ),
-      onTap: () {
-        routeToPage(context, CollectionPage(c), forceCustomPageRoute: true);
-      },
-    );
-  }
-}

+ 75 - 0
lib/ui/viewer/search/collections/files_from_holiday_page.dart

@@ -0,0 +1,75 @@
+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_load_result.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/search/holiday_search_result.dart';
+import 'package:photos/models/selected_files.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/gallery_overlay_widget.dart';
+
+class FilesFromHolidayPage extends StatelessWidget {
+  final HolidaySearchResult holidaySearchResult;
+  final String tagPrefix;
+
+  final _selectedFiles = SelectedFiles();
+  static const GalleryType appBarType = GalleryType.searchResults;
+  static const GalleryType overlayType = GalleryType.searchResults;
+
+  FilesFromHolidayPage(
+    this.holidaySearchResult,
+    this.tagPrefix, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
+        final result = holidaySearchResult.files
+            .where(
+              (file) =>
+                  file.creationTime >= creationStartTime &&
+                  file.creationTime <= creationEndTime,
+            )
+            .toList();
+        return Future.value(
+          FileLoadResult(
+            result,
+            result.length < holidaySearchResult.files.length,
+          ),
+        );
+      },
+      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
+      removalEventTypes: const {
+        EventType.deletedFromRemote,
+        EventType.deletedFromEverywhere,
+      },
+      tagPrefix: tagPrefix,
+      selectedFiles: _selectedFiles,
+      initialFiles: [holidaySearchResult.files[0]],
+    );
+    return Scaffold(
+      appBar: PreferredSize(
+        preferredSize: const Size.fromHeight(50.0),
+        child: GalleryAppBarWidget(
+          appBarType,
+          holidaySearchResult.holidayName,
+          _selectedFiles,
+        ),
+      ),
+      body: Stack(
+        alignment: Alignment.bottomCenter,
+        children: [
+          gallery,
+          GalleryOverlayWidget(
+            overlayType,
+            _selectedFiles,
+          )
+        ],
+      ),
+    );
+  }
+}

+ 75 - 0
lib/ui/viewer/search/collections/files_from_month_page.dart

@@ -0,0 +1,75 @@
+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_load_result.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/search/month_search_result.dart';
+import 'package:photos/models/selected_files.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/gallery_overlay_widget.dart';
+
+class FilesFromMonthPage extends StatelessWidget {
+  final MonthSearchResult monthSearchResult;
+  final String tagPrefix;
+
+  final _selectedFiles = SelectedFiles();
+  static const GalleryType appBarType = GalleryType.searchResults;
+  static const GalleryType overlayType = GalleryType.searchResults;
+
+  FilesFromMonthPage(
+    this.monthSearchResult,
+    this.tagPrefix, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
+        final result = monthSearchResult.files
+            .where(
+              (file) =>
+                  file.creationTime >= creationStartTime &&
+                  file.creationTime <= creationEndTime,
+            )
+            .toList();
+        return Future.value(
+          FileLoadResult(
+            result,
+            result.length < monthSearchResult.files.length,
+          ),
+        );
+      },
+      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
+      removalEventTypes: const {
+        EventType.deletedFromRemote,
+        EventType.deletedFromEverywhere,
+      },
+      tagPrefix: tagPrefix,
+      selectedFiles: _selectedFiles,
+      initialFiles: [monthSearchResult.files[0]],
+    );
+    return Scaffold(
+      appBar: PreferredSize(
+        preferredSize: const Size.fromHeight(50.0),
+        child: GalleryAppBarWidget(
+          appBarType,
+          monthSearchResult.month,
+          _selectedFiles,
+        ),
+      ),
+      body: Stack(
+        alignment: Alignment.bottomCenter,
+        children: [
+          gallery,
+          GalleryOverlayWidget(
+            overlayType,
+            _selectedFiles,
+          )
+        ],
+      ),
+    );
+  }
+}

+ 75 - 0
lib/ui/viewer/search/collections/files_from_year_page.dart

@@ -0,0 +1,75 @@
+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_load_result.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/search/year_search_result.dart';
+import 'package:photos/models/selected_files.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/gallery_overlay_widget.dart';
+
+class FilesFromYearPage extends StatelessWidget {
+  final YearSearchResult yearSearchResult;
+  final String tagPrefix;
+
+  final _selectedFiles = SelectedFiles();
+  static const GalleryType appBarType = GalleryType.searchResults;
+  static const GalleryType overlayType = GalleryType.searchResults;
+
+  FilesFromYearPage(
+    this.yearSearchResult,
+    this.tagPrefix, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
+        final result = yearSearchResult.files
+            .where(
+              (file) =>
+                  file.creationTime >= creationStartTime &&
+                  file.creationTime <= creationEndTime,
+            )
+            .toList();
+        return Future.value(
+          FileLoadResult(
+            result,
+            result.length < yearSearchResult.files.length,
+          ),
+        );
+      },
+      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
+      removalEventTypes: const {
+        EventType.deletedFromRemote,
+        EventType.deletedFromEverywhere,
+      },
+      tagPrefix: tagPrefix,
+      selectedFiles: _selectedFiles,
+      initialFiles: [yearSearchResult.files[0]],
+    );
+    return Scaffold(
+      appBar: PreferredSize(
+        preferredSize: const Size.fromHeight(50.0),
+        child: GalleryAppBarWidget(
+          appBarType,
+          yearSearchResult.year.toString(),
+          _selectedFiles,
+        ),
+      ),
+      body: Stack(
+        alignment: Alignment.bottomCenter,
+        children: [
+          gallery,
+          GalleryOverlayWidget(
+            overlayType,
+            _selectedFiles,
+          )
+        ],
+      ),
+    );
+  }
+}

+ 75 - 0
lib/ui/viewer/search/collections/files_in_location_page.dart

@@ -0,0 +1,75 @@
+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_load_result.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/search/location_search_result.dart';
+import 'package:photos/models/selected_files.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/gallery_overlay_widget.dart';
+
+class FilesInLocationPage extends StatelessWidget {
+  final LocationSearchResult locationSearchResult;
+  final String tagPrefix;
+
+  final _selectedFiles = SelectedFiles();
+  static const GalleryType appBarType = GalleryType.searchResults;
+  static const GalleryType overlayType = GalleryType.searchResults;
+
+  FilesInLocationPage(
+    this.locationSearchResult,
+    this.tagPrefix, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
+        final result = locationSearchResult.files
+            .where(
+              (file) =>
+                  file.creationTime >= creationStartTime &&
+                  file.creationTime <= creationEndTime,
+            )
+            .toList();
+        return Future.value(
+          FileLoadResult(
+            result,
+            result.length < locationSearchResult.files.length,
+          ),
+        );
+      },
+      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
+      removalEventTypes: const {
+        EventType.deletedFromRemote,
+        EventType.deletedFromEverywhere,
+      },
+      tagPrefix: tagPrefix,
+      selectedFiles: _selectedFiles,
+      initialFiles: [locationSearchResult.files[0]],
+    );
+    return Scaffold(
+      appBar: PreferredSize(
+        preferredSize: const Size.fromHeight(50.0),
+        child: GalleryAppBarWidget(
+          appBarType,
+          locationSearchResult.location,
+          _selectedFiles,
+        ),
+      ),
+      body: Stack(
+        alignment: Alignment.bottomCenter,
+        children: [
+          gallery,
+          GalleryOverlayWidget(
+            overlayType,
+            _selectedFiles,
+          )
+        ],
+      ),
+    );
+  }
+}

+ 102 - 0
lib/ui/viewer/search/search_result_widgets/collection_result_widget.dart

@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/album_search_result.dart';
+import 'package:photos/ui/viewer/gallery/collection_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class AlbumSearchResultWidget extends StatelessWidget {
+  final AlbumSearchResult albumSearchResult;
+
+  const AlbumSearchResultWidget(this.albumSearchResult, {Key key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.end,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                albumSearchResult.collectionWithThumbnail.thumbnail,
+                "collection_search",
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'Album',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 4),
+                  SizedBox(
+                    width: 220,
+                    child: Text(
+                      albumSearchResult.collectionWithThumbnail.collection.name,
+                      style: const TextStyle(fontSize: 18),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  const SizedBox(height: 2),
+                  FutureBuilder<int>(
+                    future: FilesDB.instance.collectionFileCount(
+                      albumSearchResult.collectionWithThumbnail.collection.id,
+                    ),
+                    builder: (context, snapshot) {
+                      if (snapshot.hasData && snapshot.data > 0) {
+                        final noOfMemories = snapshot.data;
+                        return RichText(
+                          text: TextSpan(
+                            style: TextStyle(
+                              color: Theme.of(context)
+                                  .colorScheme
+                                  .searchResultsCountTextColor,
+                            ),
+                            children: [
+                              TextSpan(text: noOfMemories.toString()),
+                              TextSpan(
+                                text:
+                                    noOfMemories != 1 ? ' memories' : ' memory',
+                              ),
+                            ],
+                          ),
+                        );
+                      } else {
+                        return const SizedBox.shrink();
+                      }
+                    },
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(
+          context,
+          CollectionPage(
+            albumSearchResult.collectionWithThumbnail,
+            tagPrefix: "collection_search",
+          ),
+        );
+      },
+    );
+  }
+}

+ 77 - 0
lib/ui/viewer/search/search_result_widgets/file_result_widget.dart

@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/file_search_result.dart';
+import 'package:photos/ui/viewer/file/detail_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class FileSearchResultWidget extends StatelessWidget {
+  final FileSearchResult matchedFile;
+  const FileSearchResultWidget(this.matchedFile, {Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                matchedFile.file,
+                "file_details",
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'File',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 6),
+                  SizedBox(
+                    width: 220,
+                    child: Text(
+                      matchedFile.file.title,
+                      style: const TextStyle(fontSize: 18),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        _routeToDetailPage(matchedFile.file, context);
+      },
+    );
+  }
+
+  void _routeToDetailPage(File file, BuildContext context) {
+    final page = DetailPage(
+      DetailPageConfiguration(
+        List.unmodifiable([file]),
+        null,
+        0,
+        "file_details",
+      ),
+    );
+    routeToPage(context, page);
+  }
+}

+ 89 - 0
lib/ui/viewer/search/search_result_widgets/holiday_result_widget.dart

@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/holiday_search_result.dart';
+import 'package:photos/ui/viewer/search/collections/files_from_holiday_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class HolidaySearchResultWidget extends StatelessWidget {
+  static const String _tagPrefix = "holiday_search";
+
+  final HolidaySearchResult holidaySearchResult;
+  const HolidaySearchResultWidget(this.holidaySearchResult, {Key key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final noOfMemories = holidaySearchResult.files.length;
+    final heroTagPrefix = _tagPrefix + holidaySearchResult.holidayName;
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                holidaySearchResult.files[0],
+                heroTagPrefix,
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'Date',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                  SizedBox(
+                    width: 220,
+                    child: Text(
+                      holidaySearchResult.holidayName,
+                      style: const TextStyle(fontSize: 18),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  const SizedBox(height: 2),
+                  RichText(
+                    text: TextSpan(
+                      style: TextStyle(
+                        color: Theme.of(context)
+                            .colorScheme
+                            .searchResultsCountTextColor,
+                      ),
+                      children: [
+                        TextSpan(text: noOfMemories.toString()),
+                        TextSpan(
+                          text: noOfMemories != 1 ? ' memories' : ' memory',
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(
+          context,
+          FilesFromHolidayPage(holidaySearchResult, heroTagPrefix),
+        );
+      },
+    );
+  }
+}

+ 89 - 0
lib/ui/viewer/search/search_result_widgets/location_result_widget.dart

@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/location_search_result.dart';
+import 'package:photos/ui/viewer/search/collections/files_in_location_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class LocationSearchResultWidget extends StatelessWidget {
+  static const String _tagPrefix = "location_search";
+
+  final LocationSearchResult locationSearchResult;
+  const LocationSearchResultWidget(this.locationSearchResult, {Key key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final noOfMemories = locationSearchResult.files.length;
+    final heroTagPrefix = _tagPrefix + locationSearchResult.location;
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                locationSearchResult.files[0],
+                heroTagPrefix,
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'Location',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 6),
+                  SizedBox(
+                    width: 220,
+                    child: Text(
+                      locationSearchResult.location,
+                      style: const TextStyle(fontSize: 18),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  const SizedBox(height: 2),
+                  RichText(
+                    text: TextSpan(
+                      style: TextStyle(
+                        color: Theme.of(context)
+                            .colorScheme
+                            .searchResultsCountTextColor,
+                      ),
+                      children: [
+                        TextSpan(text: noOfMemories.toString()),
+                        TextSpan(
+                          text: noOfMemories != 1 ? ' memories' : ' memory',
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(
+          context,
+          FilesInLocationPage(locationSearchResult, heroTagPrefix),
+        );
+      },
+    );
+  }
+}

+ 89 - 0
lib/ui/viewer/search/search_result_widgets/month_result_widget.dart

@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/month_search_result.dart';
+import 'package:photos/ui/viewer/search/collections/files_from_month_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class MonthSearchResultWidget extends StatelessWidget {
+  static const String _tagPrefix = "month_search";
+
+  final MonthSearchResult monthSearchResult;
+  const MonthSearchResultWidget(this.monthSearchResult, {Key key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final noOfMemories = monthSearchResult.files.length;
+    final heroTagPrefix = _tagPrefix + monthSearchResult.month;
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                monthSearchResult.files[0],
+                heroTagPrefix,
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'Date',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                  SizedBox(
+                    width: 220,
+                    child: Text(
+                      monthSearchResult.month,
+                      style: const TextStyle(fontSize: 18),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  const SizedBox(height: 2),
+                  RichText(
+                    text: TextSpan(
+                      style: TextStyle(
+                        color: Theme.of(context)
+                            .colorScheme
+                            .searchResultsCountTextColor,
+                      ),
+                      children: [
+                        TextSpan(text: noOfMemories.toString()),
+                        TextSpan(
+                          text: noOfMemories != 1 ? ' memories' : ' memory',
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(
+          context,
+          FilesFromMonthPage(monthSearchResult, heroTagPrefix),
+        );
+      },
+    );
+  }
+}

+ 77 - 0
lib/ui/viewer/search/search_result_widgets/no_result_widget.dart

@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+
+class NoResultWidget extends StatelessWidget {
+  const NoResultWidget({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: double.infinity,
+      margin: const EdgeInsets.only(top: 8),
+      padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
+      decoration: BoxDecoration(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        borderRadius: BorderRadius.circular(8),
+        boxShadow: [
+          BoxShadow(
+            color: Colors.black.withOpacity(0.2),
+            spreadRadius: -3,
+            blurRadius: 6,
+            offset: const Offset(0, 8),
+          ),
+        ],
+      ),
+      child: Container(
+        margin: const EdgeInsets.symmetric(horizontal: 8),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Center(
+              child: Container(
+                margin: const EdgeInsets.all(8),
+                child: const Text(
+                  "No results found",
+                  style: TextStyle(
+                    fontSize: 16,
+                  ),
+                ),
+              ),
+            ),
+            Container(
+              margin: const EdgeInsets.only(top: 16),
+              child: Text(
+                "You can try searching for a different query.",
+                style: TextStyle(
+                  fontSize: 14,
+                  color: Theme.of(context)
+                      .colorScheme
+                      .defaultTextColor
+                      .withOpacity(0.5),
+                  height: 1.5,
+                ),
+              ),
+            ),
+            Container(
+              margin: const EdgeInsets.only(bottom: 20, top: 12),
+              child: Text(
+                '''\u2022 Places (e.g. "London")
+\u2022 Years and months (e.g. "2022", "January")
+\u2022 Holidays (e.g. "Christmas")
+\u2022 Album names (e.g. "Recents")''',
+                style: TextStyle(
+                  fontSize: 14,
+                  color: Theme.of(context)
+                      .colorScheme
+                      .defaultTextColor
+                      .withOpacity(0.5),
+                  height: 1.5,
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 31 - 0
lib/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/widgets.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
+
+class SearchResultThumbnailWidget extends StatelessWidget {
+  final File file;
+  final String tagPrefix;
+
+  const SearchResultThumbnailWidget(
+    this.file,
+    this.tagPrefix, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Hero(
+      tag: tagPrefix + file.tag(),
+      child: SizedBox(
+        height: 58,
+        width: 58,
+        child: ClipRRect(
+          borderRadius: BorderRadius.circular(3),
+          child: ThumbnailWidget(
+            file,
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 85 - 0
lib/ui/viewer/search/search_result_widgets/year_result_widget.dart

@@ -0,0 +1,85 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/year_search_result.dart';
+import 'package:photos/ui/viewer/search/collections/files_from_year_page.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class YearSearchResultWidget extends StatelessWidget {
+  static const String _tagPrefix = "year_search";
+
+  final YearSearchResult yearSearchResult;
+  const YearSearchResultWidget(this.yearSearchResult, {Key key})
+      : super(key: key);
+  @override
+  Widget build(BuildContext context) {
+    final noOfMemories = yearSearchResult.files.length;
+    final heroTagPrefix = _tagPrefix + yearSearchResult.year;
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: Container(
+        color: Theme.of(context).colorScheme.searchResultsColor,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SearchResultThumbnailWidget(
+                yearSearchResult.files[0],
+                heroTagPrefix,
+              ),
+              const SizedBox(width: 16),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    'Date',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Theme.of(context).colorScheme.subTextColor,
+                    ),
+                  ),
+                  const SizedBox(height: 6),
+                  Text(
+                    yearSearchResult.year,
+                    style: const TextStyle(fontSize: 18),
+                    overflow: TextOverflow.ellipsis,
+                  ),
+                  const SizedBox(height: 2),
+                  RichText(
+                    text: TextSpan(
+                      style: TextStyle(
+                        color: Theme.of(context)
+                            .colorScheme
+                            .searchResultsCountTextColor,
+                      ),
+                      children: [
+                        TextSpan(text: noOfMemories.toString()),
+                        TextSpan(
+                          text: noOfMemories != 1 ? ' memories' : ' memory',
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+              const Spacer(),
+              Icon(
+                Icons.chevron_right,
+                color: Theme.of(context).colorScheme.subTextColor,
+              ),
+            ],
+          ),
+        ),
+      ),
+      onTap: () {
+        routeToPage(
+          context,
+          FilesFromYearPage(yearSearchResult, heroTagPrefix),
+        );
+      },
+    );
+  }
+}

+ 0 - 29
lib/ui/viewer/search/search_results_suggestions.dart

@@ -1,29 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-import 'package:photos/models/collection_items.dart';
-import 'package:photos/ui/viewer/search/collection_result_widget.dart';
-
-class SearchResultsSuggestions extends StatelessWidget {
-  final List<CollectionWithThumbnail> collectionsWithThumbnail;
-  const SearchResultsSuggestions({Key key, this.collectionsWithThumbnail})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    List<Widget> suggestions = [];
-    for (CollectionWithThumbnail c in collectionsWithThumbnail) {
-      suggestions.add(CollectionResultWidget(c));
-    }
-
-    return Container(
-      constraints:
-          BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.6),
-      child: ListView.builder(
-        itemCount: suggestions.length,
-        itemBuilder: (context, index) {
-          return suggestions[index];
-        },
-      ),
-    );
-  }
-}

+ 60 - 0
lib/ui/viewer/search/search_suffix_icon_widget.dart

@@ -0,0 +1,60 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+
+class SearchSuffixIcon extends StatefulWidget {
+  final Timer debounceTimer;
+  const SearchSuffixIcon(this.debounceTimer, {Key key}) : super(key: key);
+
+  @override
+  State<SearchSuffixIcon> createState() => _SearchSuffixIconState();
+}
+
+class _SearchSuffixIconState extends State<SearchSuffixIcon>
+    with TickerProviderStateMixin {
+  @override
+  Widget build(BuildContext context) {
+    final controller = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 200),
+    );
+    final animation = Tween(
+      begin: 0.0,
+      end: 1.0,
+    ).animate(controller);
+    if (widget.debounceTimer == null || !widget.debounceTimer.isActive) {
+      controller.forward();
+      return FadeTransition(
+        opacity: animation,
+        child: IconButton(
+          onPressed: () {
+            Navigator.pop(context);
+          },
+          icon: Icon(
+            Icons.close,
+            color: Theme.of(context).colorScheme.iconColor.withOpacity(0.5),
+          ),
+        ),
+      );
+    } else {
+      controller.forward();
+      return FadeTransition(
+        opacity: animation,
+        child: Padding(
+          padding: const EdgeInsets.all(12),
+          child: SizedBox(
+            height: 6,
+            width: 6,
+            child: Center(
+              child: CircularProgressIndicator(
+                strokeWidth: 2,
+                color: Theme.of(context).colorScheme.iconColor.withOpacity(0.5),
+              ),
+            ),
+          ),
+        ),
+      );
+    }
+  }
+}

+ 87 - 0
lib/ui/viewer/search/search_suggestions.dart

@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/search/album_search_result.dart';
+import 'package:photos/models/search/file_search_result.dart';
+import 'package:photos/models/search/holiday_search_result.dart';
+import 'package:photos/models/search/location_search_result.dart';
+import 'package:photos/models/search/month_search_result.dart';
+import 'package:photos/models/search/search_results.dart';
+import 'package:photos/models/search/year_search_result.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/collection_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/file_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/holiday_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/location_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/month_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/year_result_widget.dart';
+
+class SearchSuggestionsWidget extends StatelessWidget {
+  final List<SearchResult> results;
+  const SearchSuggestionsWidget(
+    this.results, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+      child: Container(
+        margin: const EdgeInsets.only(top: 8),
+        decoration: BoxDecoration(
+          color: Theme.of(context).colorScheme.searchResultsColor,
+          borderRadius: BorderRadius.circular(8),
+          boxShadow: [
+            BoxShadow(
+              color: Colors.black.withOpacity(0.2),
+              spreadRadius: -3,
+              blurRadius: 6,
+              offset: const Offset(0, 8),
+            ),
+          ],
+        ),
+        child: ClipRRect(
+          borderRadius: const BorderRadius.all(Radius.circular(8)),
+          child: Container(
+            margin: const EdgeInsets.only(top: 6),
+            constraints: const BoxConstraints(
+              maxHeight: 324,
+            ),
+            child: Scrollbar(
+              child: ListView.builder(
+                physics: const ClampingScrollPhysics(),
+                shrinkWrap: true,
+                itemCount: results.length + 1,
+                itemBuilder: (context, index) {
+                  if (results.length == index) {
+                    return Container(
+                      height: 6,
+                      color: Theme.of(context).colorScheme.searchResultsColor,
+                    );
+                  }
+                  final result = results[index];
+                  if (result is AlbumSearchResult) {
+                    return AlbumSearchResultWidget(result);
+                  } else if (result is LocationSearchResult) {
+                    return LocationSearchResultWidget(result);
+                  } else if (result is FileSearchResult) {
+                    return FileSearchResultWidget(result);
+                  } else if (result is YearSearchResult) {
+                    return YearSearchResultWidget(result);
+                  } else if (result is HolidaySearchResult) {
+                    return HolidaySearchResultWidget(result);
+                  } else if (result is MonthSearchResult) {
+                    return MonthSearchResultWidget(result);
+                  } else {
+                    Logger('SearchSuggestionsWidget')
+                        .info("Invalid/Unsupported value");
+                    return const SizedBox.shrink();
+                  }
+                },
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 166 - 81
lib/ui/viewer/search/search_widget.dart

@@ -1,8 +1,15 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
-import 'package:photos/models/collection_items.dart';
-import 'package:photos/services/collections_service.dart';
-import 'package:photos/ui/viewer/search/search_results_suggestions.dart';
+import 'package:photos/models/search/search_results.dart';
+import 'package:photos/services/search_service.dart';
+import 'package:photos/ui/viewer/search/search_result_widgets/no_result_widget.dart';
+import 'package:photos/ui/viewer/search/search_suffix_icon_widget.dart';
+import 'package:photos/ui/viewer/search/search_suggestions.dart';
+import 'package:photos/utils/date_time_util.dart';
+import 'package:photos/utils/navigation_util.dart';
+import 'package:photos/utils/search_debouncer.dart';
 
 class SearchIconWidget extends StatefulWidget {
   const SearchIconWidget({Key key}) : super(key: key);
@@ -12,104 +19,182 @@ class SearchIconWidget extends StatefulWidget {
 }
 
 class _SearchIconWidgetState extends State<SearchIconWidget> {
-  bool showSearchWidget;
   @override
   void initState() {
     super.initState();
-    showSearchWidget = false;
   }
 
   @override
   Widget build(BuildContext context) {
-    return showSearchWidget
-        ? Searchwidget(showSearchWidget)
-        : IconButton(
-            onPressed: () {
-              setState(
-                () {
-                  showSearchWidget = !showSearchWidget;
-                },
-              );
-            },
-            icon: const Icon(Icons.search),
+    return Hero(
+      tag: "search_icon",
+      child: IconButton(
+        onPressed: () {
+          Navigator.push(
+            context,
+            TransparentRoute(
+              builder: (BuildContext context) => const SearchWidget(),
+            ),
           );
+        },
+        icon: const Icon(Icons.search),
+      ),
+    );
   }
 }
 
-// ignore: must_be_immutable
-class Searchwidget extends StatefulWidget {
-  bool openSearch;
-  final String searchQuery = '';
-  Searchwidget(this.openSearch, {Key key}) : super(key: key);
+class SearchWidget extends StatefulWidget {
+  const SearchWidget({Key key}) : super(key: key);
   @override
-  State<Searchwidget> createState() => _SearchwidgetState();
+  State<SearchWidget> createState() => _SearchWidgetState();
 }
 
-class _SearchwidgetState extends State<Searchwidget> {
-  final ValueNotifier<String> _searchQ = ValueNotifier('');
+class _SearchWidgetState extends State<SearchWidget> {
+  String _query = "";
+  final List<SearchResult> _results = [];
+  final _searchService = SearchService.instance;
+  final _debouncer = Debouncer(const Duration(milliseconds: 200));
+
   @override
   Widget build(BuildContext context) {
-    List<CollectionWithThumbnail> matchedCollections;
-    return widget.openSearch
-        ? Column(
-            children: [
-              Row(
-                children: [
-                  const SizedBox(width: 12),
-                  Flexible(
-                    child: Container(
-                      color:
-                          Theme.of(context).colorScheme.defaultBackgroundColor,
-                      child: TextFormField(
-                        style: Theme.of(context).textTheme.subtitle1,
-                        decoration: InputDecoration(
-                          filled: true,
-                          contentPadding: const EdgeInsets.symmetric(
-                            horizontal: 16,
-                            vertical: 14,
-                          ),
-                          border: UnderlineInputBorder(
-                            borderSide: BorderSide.none,
-                            borderRadius: BorderRadius.circular(8),
+    return GestureDetector(
+      onTap: () {
+        Navigator.pop(context);
+      },
+      child: Container(
+        color: Colors.black.withOpacity(0.32),
+        child: SafeArea(
+          child: Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 4),
+            child: Column(
+              children: [
+                const SizedBox(height: 8),
+                ClipRRect(
+                  borderRadius: BorderRadius.circular(8),
+                  child: Container(
+                    color: Theme.of(context).colorScheme.defaultBackgroundColor,
+                    child: TextFormField(
+                      style: Theme.of(context).textTheme.subtitle1,
+                      // Below parameters are to disable auto-suggestion
+                      enableSuggestions: false,
+                      autocorrect: false,
+                      keyboardType: TextInputType.visiblePassword,
+                      // Above parameters are to disable auto-suggestion
+                      decoration: InputDecoration(
+                        hintText: "Places, moments, albums...",
+                        filled: true,
+                        contentPadding: const EdgeInsets.symmetric(
+                          horizontal: 16,
+                          vertical: 14,
+                        ),
+                        border: const UnderlineInputBorder(
+                          borderSide: BorderSide.none,
+                        ),
+                        focusedBorder: const UnderlineInputBorder(
+                          borderSide: BorderSide.none,
+                        ),
+                        prefixIcon: Hero(
+                          tag: "search_icon",
+                          child: Icon(
+                            Icons.search,
+                            color: Theme.of(context)
+                                .colorScheme
+                                .iconColor
+                                .withOpacity(0.5),
                           ),
-                          prefixIcon: const Icon(Icons.search),
                         ),
-                        onChanged: (value) async {
-                          matchedCollections = await CollectionsService.instance
-                              .getFilteredCollectionsWithThumbnail(value);
-                          _searchQ.value = value;
-                        },
-                        autofocus: true,
+                        suffixIcon: ValueListenableBuilder(
+                          valueListenable: _debouncer.debounceNotifierGetter,
+                          builder: (
+                            BuildContext context,
+                            Timer debounceTimer,
+                            Widget child,
+                          ) {
+                            return SearchSuffixIcon(debounceTimer);
+                          },
+                        ),
                       ),
+                      onChanged: (value) async {
+                        final List<SearchResult> allResults =
+                            await getSearchResultsForQuery(value);
+                        if (mounted) {
+                          setState(() {
+                            _query = value;
+                            _results.clear();
+                            _results.addAll(allResults);
+                          });
+                        }
+                      },
+                      autofocus: true,
                     ),
                   ),
-                  IconButton(
-                    onPressed: () {
-                      setState(() {
-                        widget.openSearch = !widget.openSearch;
-                      });
-                    },
-                    icon: const Icon(Icons.close),
-                  ),
-                ],
-              ),
-              const SizedBox(height: 20),
-              ValueListenableBuilder(
-                valueListenable: _searchQ,
-                builder: (
-                  BuildContext context,
-                  String newQuery,
-                  Widget child,
-                ) {
-                  return newQuery != ''
-                      ? SearchResultsSuggestions(
-                          collectionsWithThumbnail: matchedCollections,
-                        )
-                      : const SizedBox.shrink();
-                },
-              ),
-            ],
-          )
-        : SearchIconWidget();
+                ),
+                _results.isNotEmpty
+                    ? SearchSuggestionsWidget(_results)
+                    : _query.isNotEmpty
+                        ? const NoResultWidget()
+                        : const SizedBox.shrink(),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _debouncer.cancel();
+    super.dispose();
+  }
+
+  Future<List<SearchResult>> getSearchResultsForQuery(String query) async {
+    final List<SearchResult> allResults = [];
+    if (query.isEmpty) {
+      if (_debouncer.isActive()) {
+        _debouncer.cancel();
+      }
+      return (allResults);
+    }
+
+    final Completer<List<SearchResult>> completer = Completer();
+
+    _debouncer.run(() {
+      _getSearchResultsFromService(query, completer, allResults);
+    });
+
+    return completer.future;
+  }
+
+  void _getSearchResultsFromService(
+    String query,
+    Completer completer,
+    List<SearchResult> allResults,
+  ) async {
+    if (_isYearValid(query)) {
+      final yearResults = await _searchService.getYearSearchResults(query);
+      allResults.addAll(yearResults);
+    }
+
+    final holidayResults = await _searchService.getHolidaySearchResults(query);
+    allResults.addAll(holidayResults);
+
+    final collectionResults =
+        await _searchService.getCollectionSearchResults(query);
+    allResults.addAll(collectionResults);
+
+    final locationResults =
+        await _searchService.getLocationSearchResults(query);
+    allResults.addAll(locationResults);
+
+    final monthResults = await _searchService.getMonthSearchResults(query);
+    allResults.addAll(monthResults);
+
+    completer.complete(allResults);
+  }
+
+  bool _isYearValid(String year) {
+    final yearAsInt = int.tryParse(year); //returns null if cannot be parsed
+    return yearAsInt != null && yearAsInt <= currentYear;
   }
 }

+ 2 - 0
lib/utils/date_time_util.dart

@@ -41,6 +41,8 @@ Map<int, String> _days = {
   7: "Sun",
 };
 
+final currentYear = int.parse(DateTime.now().year.toString());
+
 //Jun 2022
 String getMonthAndYear(DateTime dateTime) {
   return _months[dateTime.month] + " " + dateTime.year.toString();

+ 95 - 2
lib/utils/navigation_util.dart

@@ -13,8 +13,8 @@ Future<T> routeToPage<T extends Object>(
     );
   } else {
     return Navigator.of(context).push(
-      MaterialPageRoute(
-        builder: (BuildContext context) {
+      SwipeableRouteBuilder(
+        pageBuilder: (context, animation, secondaryAnimation) {
           return page;
         },
       ),
@@ -54,3 +54,96 @@ PageRouteBuilder<T> _buildPageRoute<T extends Object>(Widget page) {
     opaque: false,
   );
 }
+
+class SwipeableRouteBuilder<T> extends PageRoute<T> {
+  final RoutePageBuilder pageBuilder;
+  final PageTransitionsBuilder matchingBuilder =
+      const CupertinoPageTransitionsBuilder(); // Default iOS/macOS (to get the swipe right to go back gesture)
+  // final PageTransitionsBuilder matchingBuilder = const FadeUpwardsPageTransitionsBuilder(); // Default Android/Linux/Windows
+
+  SwipeableRouteBuilder({this.pageBuilder});
+
+  @override
+  Color get barrierColor => null;
+
+  @override
+  String get barrierLabel => null;
+
+  @override
+  Widget buildPage(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+  ) {
+    return pageBuilder(context, animation, secondaryAnimation);
+  }
+
+  @override
+  bool get maintainState => true;
+
+  @override
+  Duration get transitionDuration => const Duration(
+        milliseconds: 300,
+      ); // Can give custom Duration, unlike in MaterialPageRoute
+
+  @override
+  Widget buildTransitions(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return matchingBuilder.buildTransitions<T>(
+      this,
+      context,
+      animation,
+      secondaryAnimation,
+      child,
+    );
+  }
+
+  @override
+  bool get opaque => false;
+}
+
+class TransparentRoute extends PageRoute<void> {
+  TransparentRoute({
+    @required this.builder,
+    RouteSettings settings,
+  })  : assert(builder != null),
+        super(settings: settings, fullscreenDialog: false);
+
+  final WidgetBuilder builder;
+
+  @override
+  bool get opaque => false;
+
+  @override
+  Color get barrierColor => null;
+
+  @override
+  String get barrierLabel => null;
+
+  @override
+  bool get maintainState => true;
+
+  @override
+  Duration get transitionDuration => const Duration(milliseconds: 200);
+
+  @override
+  Widget buildPage(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+  ) {
+    final result = builder(context);
+    return FadeTransition(
+      opacity: Tween<double>(begin: 0, end: 1).animate(animation),
+      child: Semantics(
+        scopesRoute: true,
+        explicitChildNodes: true,
+        child: result,
+      ),
+    );
+  }
+}

+ 32 - 0
lib/utils/search_debouncer.dart

@@ -0,0 +1,32 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class Debouncer {
+  final Duration _duration;
+  Timer _debounceTimer;
+  final ValueNotifier<Timer> _debounceNotifier = ValueNotifier(null);
+  Debouncer(this._duration);
+
+  void run(Function fn) {
+    if (_debounceTimer != null && _debounceTimer.isActive) {
+      _debounceTimer.cancel();
+    }
+    _debounceTimer = Timer(_duration, fn);
+    _debounceNotifier.value = _debounceTimer;
+  }
+
+  void cancel() {
+    if (_debounceTimer != null) {
+      _debounceTimer.cancel();
+    }
+  }
+
+  bool isActive() {
+    return (_debounceTimer != null) && _debounceTimer.isActive;
+  }
+
+  ValueNotifier<Timer> get debounceNotifierGetter {
+    return _debounceNotifier;
+  }
+}

+ 1 - 1
pubspec.yaml

@@ -11,7 +11,7 @@ description: ente photos application
 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 0.6.20+350
+version: 0.6.25+355
 
 environment:
   sdk: ">=2.10.0 <3.0.0"