Selaa lähdekoodia

Merge branch 'master' into empty_albums

Neeraj Gupta 2 vuotta sitten
vanhempi
commit
bbbc61fd4e
100 muutettua tiedostoa jossa 3162 lisäystä ja 1562 poistoa
  1. 33 0
      .github/workflows/code_quality.yml
  2. 2 0
      analysis_options.yaml
  3. 2 2
      android/app/build.gradle
  4. 55 29
      android/app/src/main/AndroidManifest.xml
  5. BIN
      assets/2.0x/storage_card_background.png
  6. BIN
      assets/3.0x/storage_card_background.png
  7. BIN
      assets/storage_card_background.png
  8. 1 1
      ios/Podfile.lock
  9. 1 0
      lib/core/constants.dart
  10. 4 2
      lib/core/network.dart
  11. 4 2
      lib/db/device_files_db.dart
  12. 86 36
      lib/db/files_db.dart
  13. 2 0
      lib/events/files_updated_event.dart
  14. 6 0
      lib/events/tab_changed_event.dart
  15. 57 0
      lib/models/api/collection/create_request.dart
  16. 11 0
      lib/models/collection.dart
  17. 14 14
      lib/models/file.dart
  18. 1 0
      lib/models/gallery_type.dart
  19. 25 2
      lib/models/magic_metadata.dart
  20. 1 0
      lib/models/search/search_result.dart
  21. 17 0
      lib/models/user_details.dart
  22. 41 6
      lib/services/collections_service.dart
  23. 2 4
      lib/services/feature_flag_service.dart
  24. 139 0
      lib/services/hidden_service.dart
  25. 4 2
      lib/services/local/local_sync_util.dart
  26. 3 3
      lib/services/memories_service.dart
  27. 10 6
      lib/services/remote_sync_service.dart
  28. 39 16
      lib/services/search_service.dart
  29. 34 4
      lib/theme/colors.dart
  30. 19 1
      lib/theme/text_style.dart
  31. 130 124
      lib/ui/account/recovery_key_page.dart
  32. 3 12
      lib/ui/account/verify_recovery_page.dart
  33. 143 0
      lib/ui/backup_settings_screen.dart
  34. 104 0
      lib/ui/collections/archived_collections_button_widget.dart
  35. 1 1
      lib/ui/collections/collection_item_widget.dart
  36. 0 49
      lib/ui/collections/ente_section_title.dart
  37. 29 43
      lib/ui/collections/hidden_collections_button_widget.dart
  38. 42 16
      lib/ui/collections/section_title.dart
  39. 6 4
      lib/ui/collections_gallery_widget.dart
  40. 13 4
      lib/ui/common/loading_widget.dart
  41. 0 34
      lib/ui/components/brand_title_widget.dart
  42. 1 1
      lib/ui/components/captioned_text_widget.dart
  43. 59 0
      lib/ui/components/divider_widget.dart
  44. 30 24
      lib/ui/components/expandable_menu_item_widget.dart
  45. 18 26
      lib/ui/components/home_header_widget.dart
  46. 108 0
      lib/ui/components/icon_button_widget.dart
  47. 61 11
      lib/ui/components/menu_item_widget.dart
  48. 20 0
      lib/ui/components/menu_section_description_widget.dart
  49. 11 18
      lib/ui/components/notification_warning_widget.dart
  50. 55 0
      lib/ui/components/title_bar_title_widget.dart
  51. 149 0
      lib/ui/components/title_bar_widget.dart
  52. 112 15
      lib/ui/components/toggle_switch_widget.dart
  53. 21 4
      lib/ui/create_collection_page.dart
  54. 0 0
      lib/ui/home/grant_permissions_widget.dart
  55. 0 0
      lib/ui/home/header_error_widget.dart
  56. 26 0
      lib/ui/home/header_widget.dart
  57. 189 0
      lib/ui/home/home_bottom_nav_bar.dart
  58. 88 0
      lib/ui/home/home_gallery_widget.dart
  59. 0 0
      lib/ui/home/landing_page_widget.dart
  60. 5 3
      lib/ui/home/memories_widget.dart
  61. 2 4
      lib/ui/home/preserve_footer_widget.dart
  62. 63 0
      lib/ui/home/start_backup_hook_widget.dart
  63. 6 6
      lib/ui/home/status_bar_widget.dart
  64. 51 402
      lib/ui/home_widget.dart
  65. 4 1
      lib/ui/huge_listview/draggable_scrollbar.dart
  66. 22 2
      lib/ui/huge_listview/huge_listview.dart
  67. 1 15
      lib/ui/nav_bar.dart
  68. 1 1
      lib/ui/payment/skip_subscription_widget.dart
  69. 0 2
      lib/ui/payment/stripe_subscription_page.dart
  70. 1 1
      lib/ui/payment/subscription_common_widgets.dart
  71. 1 1
      lib/ui/payment/subscription_page.dart
  72. 1 1
      lib/ui/payment/subscription_plan_widget.dart
  73. 4 0
      lib/ui/settings/about_section_widget.dart
  74. 4 0
      lib/ui/settings/account_section_widget.dart
  75. 0 1
      lib/ui/settings/app_version_widget.dart
  76. 16 53
      lib/ui/settings/backup_section_widget.dart
  77. 3 0
      lib/ui/settings/danger_section_widget.dart
  78. 4 0
      lib/ui/settings/debug_section_widget.dart
  79. 0 243
      lib/ui/settings/details_section_widget.dart
  80. 81 85
      lib/ui/settings/security_section_widget.dart
  81. 2 4
      lib/ui/settings/settings_title_bar_widget.dart
  82. 2 0
      lib/ui/settings/social_section_widget.dart
  83. 284 0
      lib/ui/settings/storage_card_widget.dart
  84. 31 0
      lib/ui/settings/storage_error_widget.dart
  85. 27 0
      lib/ui/settings/storage_progress_widget.dart
  86. 4 0
      lib/ui/settings/support_section_widget.dart
  87. 3 1
      lib/ui/settings/theme_switch_widget.dart
  88. 3 2
      lib/ui/settings_page.dart
  89. 2 2
      lib/ui/shared_collections_gallery.dart
  90. 9 2
      lib/ui/tools/editor/image_editor_page.dart
  91. 11 5
      lib/ui/viewer/file/collections_list_of_file_widget.dart
  92. 166 35
      lib/ui/viewer/file/fading_app_bar.dart
  93. 59 14
      lib/ui/viewer/file/fading_bottom_bar.dart
  94. 107 0
      lib/ui/viewer/file/file_caption_widget.dart
  95. 68 58
      lib/ui/viewer/file/file_info_widget.dart
  96. 0 100
      lib/ui/viewer/file/raw_exif_button.dart
  97. 71 0
      lib/ui/viewer/file/raw_exif_list_tile_widget.dart
  98. 3 1
      lib/ui/viewer/file/video_widget.dart
  99. 1 1
      lib/ui/viewer/gallery/archive_page.dart
  100. 7 0
      lib/ui/viewer/gallery/collection_page.dart

+ 33 - 0
.github/workflows/code_quality.yml

@@ -0,0 +1,33 @@
+name: Check Linter Rules
+on:
+  pull_request:
+    branches:
+      - master
+jobs:
+  test:
+    if: github.event.pull_request.draft == 'false'
+    name: Check the source code
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/cache@v2
+        with:
+          path: ${{ runner.tool_cache }}/flutter
+          key: flutter-3.0.0-stable
+      # Setup the flutter environment.
+      - uses: subosito/flutter-action@v2.3.0
+        with:
+          channel: 'stable'
+          flutter-version: '3.0.0'
+
+      # Fetch sub modules
+      - run: git submodule update --init --recursive
+
+      # Get flutter dependencies.
+      - name: Install packages
+        run: flutter pub get
+
+      - name: Run Linter
+        run: flutter analyze --no-fatal-infos
+#      - name: Run Test :sed:
+#        run: flutter test

+ 2 - 0
analysis_options.yaml

@@ -55,9 +55,11 @@ analyzer:
     prefer_const_constructors: warning
     prefer_const_declarations: warning
     prefer_const_constructors_in_immutables: warning
+    prefer_final_locals: warning
     unnecessary_const: error
     cancel_subscriptions: error
 
+    invalid_dependency: info
     use_build_context_synchronously: ignore # experimental lint, requires many changes
     prefer_interpolation_to_compose_strings: ignore # later too many warnings
     prefer_double_quotes: ignore # too many warnings

+ 2 - 2
android/app/build.gradle

@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 32
+    compileSdkVersion 33
 
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
@@ -47,7 +47,7 @@ android {
     defaultConfig {
         applicationId "io.ente.photos"
         minSdkVersion 19
-        targetSdkVersion 30
+        targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

+ 55 - 29
android/app/src/main/AndroidManifest.xml

@@ -1,65 +1,91 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.ente.photos">
-    <application android:name="${applicationName}" android:label="@string/app_name" android:icon="@mipmap/launcher_icon" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" android:largeHeap="true">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="io.ente.photos">
+    <application android:name="${applicationName}"
+                 android:label="@string/app_name"
+                 android:icon="@mipmap/launcher_icon"
+                 android:usesCleartextTraffic="true"
+                 android:requestLegacyExternalStorage="true"
+                 android:allowBackup="false"
+                 android:fullBackupContent="false"
+                 android:largeHeap="true">
 
-        <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
+        <activity android:name=".MainActivity" android:launchMode="singleTop"
+                  android:theme="@style/LaunchTheme"
+                  android:exported="true"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
                 <data android:scheme="ente"/>
             </intent-filter>
 
             <!--Filter to support sharing images into our app-->
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="video/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="video/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="video/*" />
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="video/*"/>
             </intent-filter>
 
         </activity>
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
-        <meta-data android:name="flutterEmbedding" android:value="2" />
-        <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
-        <meta-data android:name="io.sentry.dsn" android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
-        <meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
+        <meta-data android:name="flutterEmbedding" android:value="2"/>
+        <meta-data android:name="asset_statements"
+                   android:resource="@string/asset_statements"/>
+        <meta-data android:name="io.sentry.dsn"
+                   android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4"/>
+        <meta-data android:name="firebase_analytics_collection_deactivated"
+                   android:value="true"/>
     </application>
 
     <!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
     <!-- https://developer.android.com/training/package-visibility/use-cases -->
     <queries>
         <intent>
-            <action android:name="android.intent.action.SENDTO" />
-            <data android:scheme="mailto" />
+            <action android:name="android.intent.action.SENDTO"/>
+            <data android:scheme="mailto"/>
         </intent>
     </queries>
     <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="com.android.vending.BILLING" />
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <uses-permission
+            android:name="android.permission.READ_MEDIA_IMAGES"/> <!-- If you want to read images-->
+    <uses-permission
+            android:name="android.permission.READ_MEDIA_VIDEO"/> <!-- If you want to read videos-->
+    <uses-permission
+            android:name="android.permission.READ_EXTERNAL_STORAGE"
+            android:maxSdkVersion="32"/>
+    <uses-permission
+            android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+            android:maxSdkVersion="29"
+            tools:ignore="ScopedStorage"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="com.android.vending.BILLING"/>
 </manifest>

BIN
assets/2.0x/storage_card_background.png


BIN
assets/3.0x/storage_card_background.png


BIN
assets/storage_card_background.png


+ 1 - 1
ios/Podfile.lock

@@ -321,7 +321,7 @@ SPEC CHECKSUMS:
   FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
   FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
   flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721

+ 1 - 0
lib/core/constants.dart

@@ -11,6 +11,7 @@ const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String roadmapURL = "https://roadmap.ente.io";
 const int microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
+const int jan011991Time = 31580904000000;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 

+ 4 - 2
lib/core/network.dart

@@ -66,8 +66,10 @@ class EnteRequestInterceptor extends Interceptor {
   @override
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
     if (kDebugMode) {
-      assert(options.baseUrl == Network.apiEndpoint,
-          "interceptor should only be used for API endpoint");
+      assert(
+        options.baseUrl == Network.apiEndpoint,
+        "interceptor should only be used for API endpoint",
+      );
     }
     // ignore: prefer_const_constructors
     options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());

+ 4 - 2
lib/db/device_files_db.dart

@@ -372,8 +372,10 @@ extension DeviceFiles on FilesDB {
         deviceCollections.add(deviceCollection);
       }
       if (includeCoverThumbnail) {
-        deviceCollections.sort((a, b) =>
-            b.thumbnail.creationTime.compareTo(a.thumbnail.creationTime));
+        deviceCollections.sort(
+          (a, b) =>
+              b.thumbnail.creationTime.compareTo(a.thumbnail.creationTime),
+        );
       }
       return deviceCollections;
     } catch (e) {

+ 86 - 36
lib/db/files_db.dart

@@ -12,7 +12,6 @@ import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
-import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/utils/file_uploader_util.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite_migration/sqflite_migration.dart';
@@ -611,17 +610,9 @@ class FilesDB {
   }) async {
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
-    String whereClause;
-    List<Object> whereArgs;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      whereClause =
-          '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnMMdVisibility = ?';
-      whereArgs = [collectionID, startTime, endTime, visibility];
-    } else {
-      whereClause =
-          '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
-      whereArgs = [collectionID, startTime, endTime];
-    }
+    const String whereClause =
+        '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
+    final List<Object> whereArgs = [collectionID, startTime, endTime];
 
     final results = await db.query(
       filesTable,
@@ -636,6 +627,43 @@ class FilesDB {
     return FileLoadResult(files, files.length == limit);
   }
 
+  Future<FileLoadResult> getFilesInCollections(
+    List<int> collectionIDs,
+    int startTime,
+    int endTime,
+    int userID, {
+    int limit,
+    bool asc,
+  }) async {
+    if (collectionIDs.isEmpty) {
+      return FileLoadResult(<File>[], false);
+    }
+    String inParam = "";
+    for (final id in collectionIDs) {
+      inParam += "'" + id.toString() + "',";
+    }
+    inParam = inParam.substring(0, inParam.length - 1);
+    final db = await instance.database;
+    final order = (asc ?? false ? 'ASC' : 'DESC');
+    final String whereClause =
+        '$columnCollectionID  IN ($inParam) AND $columnCreationTime >= ? AND '
+        '$columnCreationTime <= ? AND $columnOwnerID = ?';
+    final List<Object> whereArgs = [startTime, endTime, userID];
+
+    final results = await db.query(
+      filesTable,
+      where: whereClause,
+      whereArgs: whereArgs,
+      orderBy:
+          '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
+      limit: limit,
+    );
+    final files = convertToFiles(results);
+    final dedupeResult = _deduplicatedAndFilterIgnoredFiles(files, {});
+    _logger.info("Fetched " + dedupeResult.length.toString() + " files");
+    return FileLoadResult(files, files.length == limit);
+  }
+
   Future<List<File>> getFilesCreatedWithinDurations(
     List<List<int>> durations,
     Set<int> ignoredCollectionIDs, {
@@ -1080,7 +1108,9 @@ class FilesDB {
     final db = await instance.database;
     final count = Sqflite.firstIntValue(
       await db.rawQuery(
-        'SELECT COUNT(*) FROM $filesTable where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID',
+        'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
+        '$columnMMdVisibility'
+        ' = $visibility AND $columnOwnerID = $ownerID',
       ),
     );
     return count;
@@ -1143,25 +1173,7 @@ class FilesDB {
 
   Future<List<File>> getLatestCollectionFiles() async {
     debugPrint("Fetching latestCollectionFiles from db");
-    String query;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      query = '''
-      SELECT $filesTable.*
-      FROM $filesTable
-      INNER JOIN
-        (
-          SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
-          FROM $filesTable
-          WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS 
-          NOT -1 AND $columnMMdVisibility = $visibilityVisible AND 
-          $columnUploadedFileID IS NOT -1)
-          GROUP BY $columnCollectionID
-        ) latest_files
-        ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
-        AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
-    ''';
-    } else {
-      query = '''
+    const String query = '''
       SELECT $filesTable.*
       FROM $filesTable
       INNER JOIN
@@ -1173,9 +1185,7 @@ class FilesDB {
         ) latest_files
         ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
         AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
-
   ''';
-    }
     final db = await instance.database;
     final rows = await db.rawQuery(
       query,
@@ -1250,6 +1260,33 @@ class FilesDB {
     return result;
   }
 
+  Future<Map<int, List<File>>> getAllFilesGroupByCollectionID(
+    List<int> ids,
+  ) async {
+    final result = <int, List<File>>{};
+    if (ids.isEmpty) {
+      return result;
+    }
+    String inParam = "";
+    for (final id in ids) {
+      inParam += "'" + id.toString() + "',";
+    }
+    inParam = inParam.substring(0, inParam.length - 1);
+    final db = await instance.database;
+    final results = await db.query(
+      filesTable,
+      where: '$columnUploadedFileID IN ($inParam)',
+    );
+    final files = convertToFiles(results);
+    for (File eachFile in files) {
+      if (!result.containsKey(eachFile.collectionID)) {
+        result[eachFile.collectionID] = <File>[];
+      }
+      result[eachFile.collectionID].add(eachFile);
+    }
+    return result;
+  }
+
   Future<Set<int>> getAllCollectionIDsOfFile(
     int uploadedFileID,
   ) async {
@@ -1276,15 +1313,28 @@ class FilesDB {
     return files;
   }
 
-  Future<List<File>> getAllFilesFromDB() async {
+  Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
     final db = await instance.database;
     final List<Map<String, dynamic>> result = await db.query(filesTable);
     final List<File> files = convertToFiles(result);
     final List<File> deduplicatedFiles =
-        _deduplicatedAndFilterIgnoredFiles(files, null);
+        _deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
     return deduplicatedFiles;
   }
 
+  Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
+    final db = await instance.database;
+    final result = await db.rawQuery(
+      "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
+    );
+
+    final filesCount = <FileType, int>{};
+    for (var e in result) {
+      filesCount.addAll({getFileType(e[columnFileType]): e.values.last});
+    }
+    return filesCount;
+  }
+
   Map<String, dynamic> _getRowForFile(File file) {
     final row = <String, dynamic>{};
     if (file.generatedID != null) {

+ 2 - 0
lib/events/files_updated_event.dart

@@ -20,4 +20,6 @@ enum EventType {
   deletedFromEverywhere,
   archived,
   unarchived,
+  hide,
+  unhide,
 }

+ 6 - 0
lib/events/tab_changed_event.dart

@@ -16,3 +16,9 @@ enum TabChangedEventSource {
   collectionsPage,
   backButton,
 }
+
+class TabDoubleTapEvent extends Event {
+  final int selectedIndex;
+
+  TabDoubleTapEvent(this.selectedIndex);
+}

+ 57 - 0
lib/models/api/collection/create_request.dart

@@ -0,0 +1,57 @@
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/file_magic_service.dart';
+
+class CreateRequest {
+  String encryptedKey;
+  String keyDecryptionNonce;
+  String encryptedName;
+  String nameDecryptionNonce;
+  String type;
+  CollectionAttributes? attributes;
+  MetadataRequest? magicMetadata;
+
+  CreateRequest({
+    required this.encryptedKey,
+    required this.keyDecryptionNonce,
+    required this.encryptedName,
+    required this.nameDecryptionNonce,
+    required this.type,
+    this.attributes,
+    this.magicMetadata,
+  });
+
+  CreateRequest copyWith({
+    String? encryptedKey,
+    String? keyDecryptionNonce,
+    String? encryptedName,
+    String? nameDecryptionNonce,
+    String? type,
+    CollectionAttributes? attributes,
+    MetadataRequest? magicMetadata,
+  }) =>
+      CreateRequest(
+        encryptedKey: encryptedKey ?? this.encryptedKey,
+        keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
+        encryptedName: encryptedName ?? this.encryptedName,
+        nameDecryptionNonce: nameDecryptionNonce ?? this.nameDecryptionNonce,
+        type: type ?? this.type,
+        attributes: attributes ?? this.attributes,
+        magicMetadata: magicMetadata ?? this.magicMetadata,
+      );
+
+  Map<String, dynamic> toJson() {
+    final map = <String, dynamic>{};
+    map['encryptedKey'] = encryptedKey;
+    map['keyDecryptionNonce'] = keyDecryptionNonce;
+    map['encryptedName'] = encryptedName;
+    map['nameDecryptionNonce'] = nameDecryptionNonce;
+    map['type'] = type;
+    if (attributes != null) {
+      map['attributes'] = attributes!.toMap();
+    }
+    if (magicMetadata != null) {
+      map['magicMetadata'] = magicMetadata!.toJson();
+    }
+    return map;
+  }
+}

+ 11 - 0
lib/models/collection.dart

@@ -46,6 +46,17 @@ class Collection {
     return mMdVersion > 0 && magicMetadata.visibility == visibilityArchive;
   }
 
+  bool isHidden() {
+    if (isDefaultHidden()) {
+      return true;
+    }
+    return mMdVersion > 0 && (magicMetadata.visibility == visibilityHidden);
+  }
+
+  bool isDefaultHidden() {
+    return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
+  }
+
   static CollectionType typeFromString(String type) {
     switch (type) {
       case "folder":

+ 14 - 14
lib/models/file.dart

@@ -9,6 +9,7 @@ import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/feature_flag_service.dart';
+import 'package:photos/utils/date_time_util.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/exif_util.dart';
 // ignore: import_of_legacy_library_into_null_safe
@@ -74,16 +75,13 @@ class File extends EnteFile {
     file.location = Location(asset.latitude, asset.longitude);
     file.fileType = _fileTypeFromAsset(asset);
     file.creationTime = asset.createDateTime.microsecondsSinceEpoch;
-    if (file.creationTime == 0) {
+    if (file.creationTime == null || (file.creationTime! <= jan011991Time)) {
       try {
-        final parsedDateTime = DateTime.parse(
-          basenameWithoutExtension(file.title!)
-              .replaceAll("IMG_", "")
-              .replaceAll("VID_", "")
-              .replaceAll("DCIM_", "")
-              .replaceAll("_", " "),
-        );
-        file.creationTime = parsedDateTime.microsecondsSinceEpoch;
+        final parsedDateTime =
+            parseDateFromFileName(basenameWithoutExtension(file.title ?? ""));
+
+        file.creationTime = parsedDateTime?.microsecondsSinceEpoch ??
+            asset.modifiedDateTime.microsecondsSinceEpoch;
       } catch (e) {
         file.creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
       }
@@ -101,9 +99,7 @@ class File extends EnteFile {
         type = FileType.image;
         // PHAssetMediaSubtype.photoLive.rawValue is 8
         // This hack should go away once photos_manager support livePhotos
-        if (asset.subtype != null &&
-            asset.subtype > -1 &&
-            (asset.subtype & 8) != 0) {
+        if (asset.subtype > -1 && (asset.subtype & 8) != 0) {
           type = FileType.livePhoto;
         }
         break;
@@ -165,9 +161,9 @@ class File extends EnteFile {
         duration = asset.duration;
       }
     }
-    if (fileType == FileType.image) {
+    if (fileType == FileType.image && mediaUploadData.sourceFile != null) {
       final exifTime =
-          await getCreationTimeFromEXIF(mediaUploadData.sourceFile);
+          await getCreationTimeFromEXIF(mediaUploadData.sourceFile!);
       if (exifTime != null) {
         creationTime = exifTime.microsecondsSinceEpoch;
       }
@@ -215,6 +211,10 @@ class File extends EnteFile {
     }
   }
 
+  String? get caption {
+    return pubMagicMetadata?.caption;
+  }
+
   String get thumbnailUrl {
     final endpoint = Configuration.instance.getHttpEndpoint();
     if (endpoint != kDefaultProductionEndpoint ||

+ 1 - 0
lib/models/gallery_type.dart

@@ -1,6 +1,7 @@
 enum GalleryType {
   homepage,
   archive,
+  hidden,
   trash,
   localFolder,
   // indicator for gallery view of collections shared with the user

+ 25 - 2
lib/models/magic_metadata.dart

@@ -1,12 +1,20 @@
 import 'dart:convert';
 
+// Visibility Constants
 const visibilityVisible = 0;
 const visibilityArchive = 1;
+const visibilityHidden = 2;
+
+// Collection SubType Constants
+const subTypeDefaultHidden = 1;
 
 const magicKeyVisibility = 'visibility';
+// key for collection subType
+const subTypeKey = 'subType';
 
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedName = 'editedName';
+const pubMagicKeyCaption = "caption";
 
 class MagicMetadata {
   // 0 -> visible
@@ -32,8 +40,9 @@ class MagicMetadata {
 class PubMagicMetadata {
   int? editedTime;
   String? editedName;
+  String? caption;
 
-  PubMagicMetadata({this.editedTime, this.editedName});
+  PubMagicMetadata({this.editedTime, this.editedName, this.caption});
 
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -46,6 +55,7 @@ class PubMagicMetadata {
     return PubMagicMetadata(
       editedTime: map[pubMagicKeyEditedTime],
       editedName: map[pubMagicKeyEditedName],
+      caption: map[pubMagicKeyCaption],
     );
   }
 }
@@ -56,7 +66,19 @@ class CollectionMagicMetadata {
   // 2 -> hidden etc?
   int visibility;
 
-  CollectionMagicMetadata({required this.visibility});
+  // null/0 value -> no subType
+  // 1 -> DEFAULT_HIDDEN COLLECTION for files hidden individually
+  int? subType;
+
+  CollectionMagicMetadata({required this.visibility, this.subType});
+
+  Map<String, dynamic> toJson() {
+    final result = {magicKeyVisibility: visibility};
+    if (subType != null) {
+      result[subTypeKey] = subType!;
+    }
+    return result;
+  }
 
   factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
       CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -68,6 +90,7 @@ class CollectionMagicMetadata {
     if (map == null) return null;
     return CollectionMagicMetadata(
       visibility: map[magicKeyVisibility] ?? visibilityVisible,
+      subType: map[subTypeKey],
     );
   }
 }

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

@@ -22,5 +22,6 @@ enum ResultType {
   year,
   fileType,
   fileExtension,
+  fileCaption,
   event
 }

+ 17 - 0
lib/models/user_details.dart

@@ -2,6 +2,7 @@ import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:equatable/equatable.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/subscription.dart';
 
 class UserDetails extends Equatable {
@@ -118,3 +119,19 @@ class FamilyData {
     );
   }
 }
+
+class FilesCount {
+  final Map<FileType, int> filesCount;
+  FilesCount(this.filesCount);
+
+  int get total =>
+      images + videos + livePhotos + (filesCount[getInt(FileType.other)] ?? 0);
+
+  int get photos => images + livePhotos;
+
+  int get images => filesCount[FileType.image] ?? 0;
+
+  int get videos => filesCount[FileType.video] ?? 0;
+
+  int get livePhotos => filesCount[FileType.livePhoto] ?? 0;
+}

+ 41 - 6
lib/services/collections_service.dart

@@ -21,6 +21,7 @@ import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/models/api/collection/create_request.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/collection_items.dart';
@@ -51,6 +52,7 @@ class CollectionsService {
   final _localPathToCollectionID = <String, int>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _cachedKeys = <int, Uint8List>{};
+  Collection cachedDefaultHiddenCollection;
 
   CollectionsService._privateConstructor() {
     _db = CollectionsDB.instance;
@@ -78,6 +80,15 @@ class CollectionsService {
     });
   }
 
+  Configuration get config => _config;
+
+  Map<int, Collection> get collectionIDToCollections =>
+      _collectionIDToCollections;
+
+  FilesDB get filesDB => _filesDB;
+
+  // sync method fetches just sync the collections, not the individual files
+  // within the collection.
   Future<List<Collection>> sync() async {
     _logger.info("Syncing collections");
     final lastCollectionUpdationTime =
@@ -145,6 +156,22 @@ class CollectionsService {
         .toSet();
   }
 
+  Set<int> getHiddenCollections() {
+    return _collectionIDToCollections.values
+        .toList()
+        .where((element) => element.isHidden())
+        .map((e) => e.id)
+        .toSet();
+  }
+
+  Set<int> collectionsHiddenFromTimeline() {
+    return _collectionIDToCollections.values
+        .toList()
+        .where((element) => element.isHidden() || element.isArchived())
+        .map((e) => e.id)
+        .toSet();
+  }
+
   int getCollectionSyncTime(int collectionID) {
     return _prefs
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
@@ -177,6 +204,8 @@ class CollectionsService {
   }) async {
     final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
     final usersCollection = getActiveCollections();
+    // remove any hidden collection to avoid accidental rendering on UI
+    usersCollection.removeWhere((element) => element.isHidden());
     if (!includedOwnedByOthers) {
       final userID = Configuration.instance.getUserID();
       usersCollection.removeWhere((c) => c.owner.id != userID);
@@ -298,6 +327,7 @@ class CollectionsService {
   }
 
   Uint8List _getDecryptedKey(Collection collection) {
+    debugPrint("Finding collection decryption key for ${collection.id}");
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
     if (collection.owner.id == _config.getUserID()) {
       if (_config.getKey() == null) {
@@ -820,17 +850,17 @@ class CollectionsService {
     List<File> files,
   ) {
     if (toCollectionID == fromCollectionID) {
-      throw AssertionError("can't move to same album");
+      throw AssertionError("Can't move to same album");
     }
     for (final file in files) {
       if (file.uploadedFileID == null) {
-        throw AssertionError("can only move uploaded memories");
+        throw AssertionError("Can only move uploaded memories");
       }
       if (file.collectionID != fromCollectionID) {
-        throw AssertionError("all memories should belong to the same album");
+        throw AssertionError("All memories should belong to the same album");
       }
       if (file.ownerID != Configuration.instance.getUserID()) {
-        throw AssertionError("can only move memories uploaded by you");
+        throw AssertionError("Can only move memories uploaded by you");
       }
     }
   }
@@ -854,11 +884,16 @@ class CollectionsService {
     RemoteSyncService.instance.sync(silently: true);
   }
 
-  Future<Collection> createAndCacheCollection(Collection collection) async {
+  Future<Collection> createAndCacheCollection(
+    Collection collection, {
+    CreateRequest createRequest,
+  }) async {
+    final dynamic payload =
+        createRequest != null ? createRequest.toJson() : collection.toMap();
     return _enteDio
         .post(
       "/collections",
-      data: collection.toMap(),
+      data: payload,
     )
         .then((response) {
       final collection = Collection.fromMap(response.data["collection"]);

+ 2 - 4
lib/services/feature_flag_service.dart

@@ -73,10 +73,8 @@ class FeatureFlagService {
           .getDio()
           .get("https://static.ente.io/feature_flags.json");
       final flagsResponse = FeatureFlags.fromMap(response.data);
-      if (flagsResponse != null) {
-        _prefs.setString(_featureFlagsKey, flagsResponse.toJson());
-        _featureFlags = flagsResponse;
-      }
+      _prefs.setString(_featureFlagsKey, flagsResponse.toJson());
+      _featureFlags = flagsResponse;
     } catch (e) {
       _logger.severe("Failed to sync feature flags ", e);
     }

+ 139 - 0
lib/services/hidden_service.dart

@@ -0,0 +1,139 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_sodium/flutter_sodium.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/events/force_reload_home_gallery_event.dart';
+import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/models/api/collection/create_request.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/file_magic_service.dart';
+import 'package:photos/utils/crypto_util.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+extension HiddenService on CollectionsService {
+  static final _logger = Logger("HiddenCollectionService");
+
+  // getDefaultHiddenCollection will return null if there's no default
+  // collection
+  Future<Collection> getDefaultHiddenCollection() async {
+    if (cachedDefaultHiddenCollection != null) {
+      return cachedDefaultHiddenCollection;
+    }
+    final int userID = config.getUserID()!;
+    final Collection? defaultHidden =
+        collectionIDToCollections.values.firstWhereOrNull(
+      (element) => element.isDefaultHidden() && element.owner!.id == userID,
+    );
+    if (defaultHidden != null) {
+      cachedDefaultHiddenCollection = defaultHidden;
+      return cachedDefaultHiddenCollection;
+    }
+    final Collection createdHiddenCollection =
+        await _createDefaultHiddenAlbum();
+    cachedDefaultHiddenCollection = createdHiddenCollection;
+    return cachedDefaultHiddenCollection;
+  }
+
+  Future<bool> hideFiles(
+    BuildContext context,
+    List<File> filesToHide, {
+    bool forceHide = false,
+  }) async {
+    final int userID = config.getUserID()!;
+    final List<int> uploadedIDs = <int>[];
+    final dialog = createProgressDialog(
+      context,
+      "Hiding...",
+    );
+    await dialog.show();
+    try {
+      for (File file in filesToHide) {
+        if (file.uploadedFileID == null) {
+          throw AssertionError("Can only hide uploaded files");
+        }
+        if (file.ownerID != userID) {
+          throw AssertionError("Can only hide files owned by user");
+        }
+        uploadedIDs.add(file.uploadedFileID!);
+      }
+
+      final defaultHiddenCollection = await getDefaultHiddenCollection();
+      final Map<int, List<File>> collectionToFilesMap =
+          await filesDB.getAllFilesGroupByCollectionID(uploadedIDs);
+      for (MapEntry<int, List<File>> entry in collectionToFilesMap.entries) {
+        if (entry.key == defaultHiddenCollection.id) {
+          _logger.finest('file already part of hidden collection');
+          continue;
+        }
+        await move(defaultHiddenCollection.id, entry.key, entry.value);
+      }
+      Bus.instance.fire(ForceReloadHomeGalleryEvent());
+      Bus.instance.fire(
+        LocalPhotosUpdatedEvent(filesToHide, type: EventType.unarchived),
+      );
+
+      await dialog.hide();
+    } on AssertionError catch (e) {
+      await dialog.hide();
+      showErrorDialog(context, "Oops", e.message as String);
+    } catch (e, s) {
+      _logger.severe("Could not hide", e, s);
+      await dialog.hide();
+      showGenericErrorDialog(context);
+      return false;
+    } finally {
+      await dialog.hide();
+    }
+    return true;
+  }
+
+  Future<Collection> _createDefaultHiddenAlbum() async {
+    final key = CryptoUtil.generateKey();
+    final encryptedKeyData = CryptoUtil.encryptSync(key, config.getKey()!);
+    final encryptedName = CryptoUtil.encryptSync(
+      utf8.encode(".Hidden") as Uint8List,
+      key,
+    );
+    final jsonToUpdate = CollectionMagicMetadata(
+      visibility: visibilityHidden,
+      subType: subTypeDefaultHidden,
+    ).toJson();
+    assert(jsonToUpdate.length == 2, "metadata should have two keys");
+    final encryptedMMd = await CryptoUtil.encryptChaCha(
+      utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
+      key,
+    );
+    final MetadataRequest metadataRequest = MetadataRequest(
+      version: 1,
+      count: jsonToUpdate.length,
+      data: Sodium.bin2base64(encryptedMMd.encryptedData!),
+      header: Sodium.bin2base64(encryptedMMd.header!),
+    );
+    final CreateRequest createRequest = CreateRequest(
+      encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
+      keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
+      encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
+      nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
+      type: CollectionType.album.toString(),
+      attributes: CollectionAttributes(),
+      magicMetadata: metadataRequest,
+    );
+
+    _logger.info("Creating Hidden Collection");
+    final collection =
+        await createAndCacheCollection(null, createRequest: createRequest);
+    _logger.info("Creating Hidden Collection Created Successfully");
+    final Collection collectionFromServer =
+        await fetchCollectionByID(collection.id);
+    _logger.info("Fetched Created Hidden Collection Successfully");
+    return collectionFromServer;
+  }
+}

+ 4 - 2
lib/services/local/local_sync_util.dart

@@ -264,8 +264,10 @@ Future<List<AssetEntity>> _getAllAssetLists(AssetPathEntity pathEntity) async {
       size: assetFetchPageSize,
     );
     Bus.instance.fire(
-      LocalImportProgressEvent(pathEntity.name,
-          currentPage * assetFetchPageSize + currentPageResult.length),
+      LocalImportProgressEvent(
+        pathEntity.name,
+        currentPage * assetFetchPageSize + currentPageResult.length,
+      ),
     );
     result.addAll(currentPageResult);
     currentPage = currentPage + 1;

+ 3 - 3
lib/services/memories_service.dart

@@ -74,11 +74,11 @@ class MemoriesService extends ChangeNotifier {
           date.add(const Duration(days: daysAfter)).microsecondsSinceEpoch;
       durations.add([startCreationTime, endCreationTime]);
     }
-    final archivedCollectionIds =
-        CollectionsService.instance.getArchivedCollections();
+    final ignoredCollections =
+        CollectionsService.instance.collectionsHiddenFromTimeline();
     final files = await _filesDB.getFilesCreatedWithinDurations(
       durations,
-      archivedCollectionIds,
+      ignoredCollections,
     );
     final seenTimes = await _memoriesDB.getSeenTimes();
     final List<Memory> memories = [];

+ 10 - 6
lib/services/remote_sync_service.dart

@@ -129,6 +129,7 @@ class RemoteSyncService {
           // session are not processed now
           sync();
         } else {
+          debugPrint("Fire backup completed event");
           Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
         }
       } else {
@@ -259,7 +260,6 @@ class RemoteSyncService {
         await _db.getDevicePathIDToLocalIDMap();
     bool moreFilesMarkedForBackup = false;
     for (final deviceCollection in deviceCollections) {
-      _logger.fine("processing ${deviceCollection.name}");
       final Set<String> localIDsToSync =
           pathIdToLocalIDs[deviceCollection.id] ?? {};
       if (deviceCollection.uploadStrategy == UploadStrategy.ifMissing) {
@@ -360,16 +360,20 @@ class RemoteSyncService {
       if (pendingUploads.isEmpty) {
         continue;
       } else {
-        _logger.info("RemovingFiles $collectionIDs: pendingUploads "
-            "${pendingUploads.length}");
+        _logger.info(
+          "RemovingFiles $collectionIDs: pendingUploads "
+          "${pendingUploads.length}",
+        );
       }
       final Set<String> localIDsInOtherFileEntries =
           await _db.getLocalIDsPresentInEntries(
         pendingUploads,
         collectionID,
       );
-      _logger.info("RemovingFiles $collectionIDs: filesInOtherCollection "
-          "${localIDsInOtherFileEntries.length}");
+      _logger.info(
+        "RemovingFiles $collectionIDs: filesInOtherCollection "
+        "${localIDsInOtherFileEntries.length}",
+      );
       final List<File> entriesToUpdate = [];
       final List<int> entriesToDelete = [];
       for (File pendingUpload in pendingUploads) {
@@ -400,7 +404,7 @@ class RemoteSyncService {
         if (collectionByID == null || collectionByID.isDeleted) {
           _logger.info(
             "Collection $deviceCollectionID either deleted or missing "
-            "for path ${deviceCollection.name}",
+            "for path ${deviceCollection.id}",
           );
           deviceCollectionID = -1;
         }

+ 39 - 16
lib/services/search_service.dart

@@ -32,28 +32,23 @@ class SearchService {
   static final SearchService instance = SearchService._privateConstructor();
 
   Future<void> init() async {
-    // Intention of delay is to give more CPU cycles to other tasks
-    // 8 is just a magic number
-    Future.delayed(const Duration(seconds: 8), () async {
-      /* In case home screen loads before 8 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) {
       // only invalidate, let the load happen on demand
       _cachedFilesFuture = null;
     });
   }
 
+  Set<int> ignoreCollections() {
+    return CollectionsService.instance.getHiddenCollections();
+  }
+
   Future<List<File>> _getAllFiles() async {
     if (_cachedFilesFuture != null) {
       return _cachedFilesFuture;
     }
     _logger.fine("Reading all files from db");
-    _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
+    _cachedFilesFuture =
+        FilesDB.instance.getAllFilesFromDB(ignoreCollections());
     return _cachedFilesFuture;
   }
 
@@ -133,7 +128,11 @@ class SearchService {
       if (collectionSearchResults.length >= _maximumResultsLimit) {
         break;
       }
-      if (c.collection.name.toLowerCase().contains(query.toLowerCase())) {
+
+      if (!c.collection.isHidden() &&
+          c.collection.name.toLowerCase().contains(
+                query.toLowerCase(),
+              )) {
         collectionSearchResults.add(AlbumSearchResult(c));
       }
     }
@@ -172,7 +171,7 @@ class SearchService {
         final matchedFiles =
             await FilesDB.instance.getFilesCreatedWithinDurations(
           _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
-          null,
+          ignoreCollections(),
           order: 'DESC',
         );
         if (matchedFiles.isNotEmpty) {
@@ -209,6 +208,30 @@ class SearchService {
     return searchResults;
   }
 
+  Future<List<GenericSearchResult>> getCaptionResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> searchResults = [];
+    if (query.isEmpty) {
+      return searchResults;
+    }
+    final RegExp pattern = RegExp(query, caseSensitive: false);
+    final List<File> allFiles = await _getAllFiles();
+    final matchedFiles = allFiles
+        .where((e) => e.caption != null && pattern.hasMatch(e.caption))
+        .toList();
+    if (matchedFiles.isNotEmpty) {
+      searchResults.add(
+        GenericSearchResult(
+          ResultType.fileCaption,
+          query,
+          matchedFiles,
+        ),
+      );
+    }
+    return searchResults;
+  }
+
   Future<List<GenericSearchResult>> getFileExtensionResults(
     String query,
   ) async {
@@ -248,7 +271,7 @@ class SearchService {
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsOfMonthInEveryYear(month.monthNumber),
-        null,
+        ignoreCollections(),
         order: 'DESC',
       );
       if (matchedFiles.isNotEmpty) {
@@ -277,7 +300,7 @@ class SearchService {
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsForCalendarDateInEveryYear(day, month, year: year),
-        null,
+        ignoreCollections(),
         order: 'DESC',
       );
       if (matchedFiles.isNotEmpty) {
@@ -305,7 +328,7 @@ class SearchService {
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
     return await FilesDB.instance.getFilesCreatedWithinDurations(
       [durationOfYear],
-      null,
+      ignoreCollections(),
       order: "DESC",
     );
   }

+ 34 - 4
lib/theme/colors.dart

@@ -11,11 +11,13 @@ class EnteColorScheme {
   // Backdrop Colors
   final Color backdropBase;
   final Color backdropBaseMute;
+  final Color backdropFaint;
 
   // Text Colors
   final Color textBase;
   final Color textMuted;
   final Color textFaint;
+  final Color blurTextBase;
 
   // Fill Colors
   final Color fillBase;
@@ -27,6 +29,9 @@ class EnteColorScheme {
   final Color strokeMuted;
   final Color strokeFaint;
   final Color strokeFainter;
+  final Color blurStrokeBase;
+  final Color blurStrokeFaint;
+  final Color blurStrokePressed;
 
   // Fixed Colors
   final Color primary700;
@@ -49,9 +54,11 @@ class EnteColorScheme {
     this.backgroundElevated2,
     this.backdropBase,
     this.backdropBaseMute,
+    this.backdropFaint,
     this.textBase,
     this.textMuted,
     this.textFaint,
+    this.blurTextBase,
     this.fillBase,
     this.fillMuted,
     this.fillFaint,
@@ -59,6 +66,9 @@ class EnteColorScheme {
     this.strokeMuted,
     this.strokeFaint,
     this.strokeFainter,
+    this.blurStrokeBase,
+    this.blurStrokeFaint,
+    this.blurStrokePressed,
     this.tabIcon, {
     this.primary700 = _primary700,
     this.primary500 = _primary500,
@@ -76,10 +86,12 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   backgroundElevatedLight,
   backgroundElevated2Light,
   backdropBaseLight,
-  backdropBaseMuteLight,
+  backdropMutedLight,
+  backdropFaintLight,
   textBaseLight,
   textMutedLight,
   textFaintLight,
+  blurTextBaseLight,
   fillBaseLight,
   fillMutedLight,
   fillFaintLight,
@@ -87,6 +99,9 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   strokeMutedLight,
   strokeFaintLight,
   strokeFainterLight,
+  blurStrokeBaseLight,
+  blurStrokeFaintLight,
+  blurStrokePressedLight,
   tabIconLight,
 );
 
@@ -95,10 +110,12 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   backgroundElevatedDark,
   backgroundElevated2Dark,
   backdropBaseDark,
-  backdropBaseMuteDark,
+  backdropMutedDark,
+  backdropFaintDark,
   textBaseDark,
   textMutedDark,
   textFaintDark,
+  blurTextBaseDark,
   fillBaseDark,
   fillMutedDark,
   fillFaintDark,
@@ -106,6 +123,9 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   strokeMutedDark,
   strokeFaintDark,
   strokeFainterDark,
+  blurStrokeBaseDark,
+  blurStrokeFaintDark,
+  blurStrokePressedDark,
   tabIconDark,
 );
 
@@ -120,19 +140,23 @@ const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
 
 // Backdrop Colors
 const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75);
-const Color backdropBaseMuteLight = Color.fromRGBO(255, 255, 255, 0.30);
+const Color backdropMutedLight = Color.fromRGBO(255, 255, 255, 0.30);
+const Color backdropFaintLight = Color.fromRGBO(255, 255, 255, 0.15);
 
 const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.65);
-const Color backdropBaseMuteDark = Color.fromRGBO(0, 0, 0, 0.20);
+const Color backdropMutedDark = Color.fromRGBO(0, 0, 0, 0.20);
+const Color backdropFaintDark = Color.fromRGBO(0, 0, 0, 0.08);
 
 // Text Colors
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color textMutedLight = Color.fromRGBO(0, 0, 0, 0.6);
 const Color textFaintLight = Color.fromRGBO(0, 0, 0, 0.5);
+const Color blurTextBaseLight = Color.fromRGBO(0, 0, 0, 0.65);
 
 const Color textBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color textMutedDark = Color.fromRGBO(255, 255, 255, 0.7);
 const Color textFaintDark = Color.fromRGBO(255, 255, 255, 0.5);
+const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
 
 // Fill Colors
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
@@ -148,11 +172,17 @@ const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color strokeMutedLight = Color.fromRGBO(0, 0, 0, 0.24);
 const Color strokeFaintLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color strokeFainterLight = Color.fromRGBO(0, 0, 0, 0.06);
+const Color blurStrokeBaseLight = Color.fromRGBO(0, 0, 0, 0.65);
+const Color blurStrokeFaintLight = Color.fromRGBO(0, 0, 0, 0.08);
+const Color blurStrokePressedLight = Color.fromRGBO(0, 0, 0, 0.50);
 
 const Color strokeBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color strokeMutedDark = Color.fromRGBO(255, 255, 255, 0.24);
 const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color strokeFainterDark = Color.fromRGBO(255, 255, 255, 0.08);
+const Color blurStrokeBaseDark = Color.fromRGBO(0, 0, 0, 0.90);
+const Color blurStrokeFaintDark = Color.fromRGBO(0, 0, 0, 0.08);
+const Color blurStrokePressedDark = Color.fromRGBO(0, 0, 0, 0.50);
 
 // Other colors
 const Color tabIconLight = Color.fromRGBO(0, 0, 0, 0.85);

+ 19 - 1
lib/theme/text_style.dart

@@ -5,6 +5,18 @@ const FontWeight _regularWeight = FontWeight.w500;
 const FontWeight _boldWeight = FontWeight.w600;
 const String _fontFamily = 'Inter';
 
+const TextStyle brandStyleSmall = TextStyle(
+  fontWeight: FontWeight.bold,
+  fontFamily: 'Montserrat',
+  fontSize: 21,
+);
+
+const TextStyle brandStyleMedium = TextStyle(
+  fontWeight: FontWeight.bold,
+  fontFamily: 'Montserrat',
+  fontSize: 24,
+);
+
 const TextStyle h1 = TextStyle(
   fontSize: 48,
   height: 48 / 28,
@@ -31,7 +43,7 @@ const TextStyle large = TextStyle(
 );
 const TextStyle body = TextStyle(
   fontSize: 16,
-  height: 19.4 / 16.0,
+  height: 20 / 16.0,
   fontWeight: _regularWeight,
   fontFamily: _fontFamily,
 );
@@ -71,6 +83,8 @@ class EnteTextTheme {
   final TextStyle miniBold;
   final TextStyle tiny;
   final TextStyle tinyBold;
+  final TextStyle brandSmall;
+  final TextStyle brandMedium;
 
   const EnteTextTheme({
     required this.h1,
@@ -89,6 +103,8 @@ class EnteTextTheme {
     required this.miniBold,
     required this.tiny,
     required this.tinyBold,
+    required this.brandSmall,
+    required this.brandMedium,
   });
 }
 
@@ -113,5 +129,7 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
     miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
     tiny: tiny.copyWith(color: color),
     tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
+    brandSmall: brandStyleSmall.copyWith(color: color),
+    brandMedium: brandStyleMedium.copyWith(color: color),
   );
 }

+ 130 - 124
lib/ui/account/recovery_key_page.dart

@@ -63,142 +63,148 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
             : 120;
 
     return Scaffold(
-        appBar: widget.showProgressBar
-            ? AppBar(
-                elevation: 0,
-                title: Hero(
-                  tag: "recovery_key",
-                  child: StepProgressIndicator(
-                    totalSteps: 4,
-                    currentStep: 3,
-                    selectedColor:
-                        Theme.of(context).colorScheme.greenAlternative,
-                    roundedEdges: const Radius.circular(10),
-                    unselectedColor: Theme.of(context)
-                        .colorScheme
-                        .stepProgressUnselectedColor,
-                  ),
+      appBar: widget.showProgressBar
+          ? AppBar(
+              elevation: 0,
+              title: Hero(
+                tag: "recovery_key",
+                child: StepProgressIndicator(
+                  totalSteps: 4,
+                  currentStep: 3,
+                  selectedColor: Theme.of(context).colorScheme.greenAlternative,
+                  roundedEdges: const Radius.circular(10),
+                  unselectedColor:
+                      Theme.of(context).colorScheme.stepProgressUnselectedColor,
                 ),
-              )
-            : widget.showAppBar
-                ? AppBar(
-                    elevation: 0,
-                    title: Text(widget.title ?? "Recovery key"),
-                  )
-                : null,
-        body: Padding(
-          padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20),
-          child: LayoutBuilder(
-            builder: (context, constraints) {
-              return SingleChildScrollView(
-                child: ConstrainedBox(
-                  constraints: BoxConstraints(
-                      minWidth: constraints.maxWidth,
-                      minHeight: constraints.maxHeight),
-                  child: IntrinsicHeight(
-                    child: Column(
-                      mainAxisSize: MainAxisSize.max,
-                      children: [
-                        widget.showAppBar
-                            ? const SizedBox.shrink()
-                            : Text(
-                                widget.title ?? "Recovery key",
-                                style: Theme.of(context).textTheme.headline4,
-                              ),
-                        Padding(
-                            padding:
-                                EdgeInsets.all(widget.showAppBar ? 0 : 12)),
-                        Text(
-                          widget.text ??
-                              "If you forget your password, the only way you can recover your data is with this key.",
-                          style: Theme.of(context).textTheme.subtitle1,
-                        ),
-                        const Padding(padding: EdgeInsets.only(top: 24)),
-                        DottedBorder(
-                          color: const Color.fromRGBO(17, 127, 56, 1),
-                          //color of dotted/dash line
-                          strokeWidth: 1,
-                          //thickness of dash/dots
-                          dashPattern: const [6, 6],
-                          radius: const Radius.circular(8),
-                          //dash patterns, 10 is dash width, 6 is space width
-                          child: SizedBox(
-                            //inner container
-                            // height: 120, //height of inner container
-                            width: double
-                                .infinity, //width to 100% match to parent container.
-                            // ignore: prefer_const_literals_to_create_immutables
-                            child: Column(
-                              children: [
-                                GestureDetector(
-                                  onTap: () async {
-                                    await Clipboard.setData(
-                                      ClipboardData(text: recoveryKey),
-                                    );
-                                    showToast(context,
-                                        "Recovery key copied to clipboard");
-                                    setState(() {
-                                      _hasTriedToSave = true;
-                                    });
-                                  },
-                                  child: Container(
-                                    decoration: BoxDecoration(
-                                      border: Border.all(
-                                        color: const Color.fromRGBO(
-                                            49, 155, 86, .2),
-                                      ),
-                                      borderRadius: const BorderRadius.all(
-                                        Radius.circular(2),
+              ),
+            )
+          : widget.showAppBar
+              ? AppBar(
+                  elevation: 0,
+                  title: Text(widget.title ?? "Recovery key"),
+                )
+              : null,
+      body: Padding(
+        padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20),
+        child: LayoutBuilder(
+          builder: (context, constraints) {
+            return SingleChildScrollView(
+              child: ConstrainedBox(
+                constraints: BoxConstraints(
+                  minWidth: constraints.maxWidth,
+                  minHeight: constraints.maxHeight,
+                ),
+                child: IntrinsicHeight(
+                  child: Column(
+                    mainAxisSize: MainAxisSize.max,
+                    children: [
+                      widget.showAppBar
+                          ? const SizedBox.shrink()
+                          : Text(
+                              widget.title ?? "Recovery key",
+                              style: Theme.of(context).textTheme.headline4,
+                            ),
+                      Padding(
+                        padding: EdgeInsets.all(widget.showAppBar ? 0 : 12),
+                      ),
+                      Text(
+                        widget.text ??
+                            "If you forget your password, the only way you can recover your data is with this key.",
+                        style: Theme.of(context).textTheme.subtitle1,
+                      ),
+                      const Padding(padding: EdgeInsets.only(top: 24)),
+                      DottedBorder(
+                        color: const Color.fromRGBO(17, 127, 56, 1),
+                        //color of dotted/dash line
+                        strokeWidth: 1,
+                        //thickness of dash/dots
+                        dashPattern: const [6, 6],
+                        radius: const Radius.circular(8),
+                        //dash patterns, 10 is dash width, 6 is space width
+                        child: SizedBox(
+                          //inner container
+                          // height: 120, //height of inner container
+                          width: double
+                              .infinity, //width to 100% match to parent container.
+                          // ignore: prefer_const_literals_to_create_immutables
+                          child: Column(
+                            children: [
+                              GestureDetector(
+                                onTap: () async {
+                                  await Clipboard.setData(
+                                    ClipboardData(text: recoveryKey),
+                                  );
+                                  showToast(
+                                    context,
+                                    "Recovery key copied to clipboard",
+                                  );
+                                  setState(() {
+                                    _hasTriedToSave = true;
+                                  });
+                                },
+                                child: Container(
+                                  decoration: BoxDecoration(
+                                    border: Border.all(
+                                      color: const Color.fromRGBO(
+                                        49,
+                                        155,
+                                        86,
+                                        .2,
                                       ),
-                                      color: Theme.of(context)
-                                          .colorScheme
-                                          .recoveryKeyBoxColor,
                                     ),
-                                    padding: const EdgeInsets.all(20),
-                                    width: double.infinity,
-                                    child: Text(
-                                      recoveryKey,
-                                      style:
-                                          Theme.of(context).textTheme.bodyText1,
+                                    borderRadius: const BorderRadius.all(
+                                      Radius.circular(2),
                                     ),
+                                    color: Theme.of(context)
+                                        .colorScheme
+                                        .recoveryKeyBoxColor,
+                                  ),
+                                  padding: const EdgeInsets.all(20),
+                                  width: double.infinity,
+                                  child: Text(
+                                    recoveryKey,
+                                    style:
+                                        Theme.of(context).textTheme.bodyText1,
                                   ),
                                 ),
-                              ],
-                            ),
+                              ),
+                            ],
                           ),
                         ),
-                        SizedBox(
-                          height: 80,
-                          width: double.infinity,
-                          child: Padding(
-                            padding: const EdgeInsets.symmetric(vertical: 20),
-                            child: Text(
-                              widget.subText ??
-                                  "We don’t store this key, please save this in a safe place.",
-                              style: Theme.of(context).textTheme.bodyText1,
-                            ),
+                      ),
+                      SizedBox(
+                        height: 80,
+                        width: double.infinity,
+                        child: Padding(
+                          padding: const EdgeInsets.symmetric(vertical: 20),
+                          child: Text(
+                            widget.subText ??
+                                "We don’t store this key, please save this in a safe place.",
+                            style: Theme.of(context).textTheme.bodyText1,
                           ),
                         ),
-                        Expanded(
-                          child: Container(
-                            alignment: Alignment.bottomCenter,
-                            width: double.infinity,
-                            padding: const EdgeInsets.fromLTRB(10, 10, 10, 42),
-                            child: Column(
-                              mainAxisAlignment: MainAxisAlignment.end,
-                              crossAxisAlignment: CrossAxisAlignment.stretch,
-                              children: _saveOptions(context, recoveryKey),
-                            ),
+                      ),
+                      Expanded(
+                        child: Container(
+                          alignment: Alignment.bottomCenter,
+                          width: double.infinity,
+                          padding: const EdgeInsets.fromLTRB(10, 10, 10, 42),
+                          child: Column(
+                            mainAxisAlignment: MainAxisAlignment.end,
+                            crossAxisAlignment: CrossAxisAlignment.stretch,
+                            children: _saveOptions(context, recoveryKey),
                           ),
-                        )
-                      ],
-                    ), // columnEnds
-                  ),
+                        ),
+                      )
+                    ],
+                  ), // columnEnds
                 ),
-              );
-            },
-          ),
-        ));
+              ),
+            );
+          },
+        ),
+      ),
+    );
   }
 
   List<Widget> _saveOptions(BuildContext context, String recoveryKey) {

+ 3 - 12
lib/ui/account/verify_recovery_page.dart

@@ -148,16 +148,14 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                       SizedBox(
                         width: double.infinity,
                         child: Text(
-                          'Verify recovery key',
+                          'Confirm recovery key',
                           style: enteTheme.textTheme.h3Bold,
                           textAlign: TextAlign.left,
                         ),
                       ),
                       const SizedBox(height: 18),
                       Text(
-                        "If you forget your password, your recovery key is the "
-                        "only way to recover your photos.\n\nPlease verify that "
-                        "you have safely backed up your 24 word recovery key by re-entering it.",
+                        "Your recovery key is the only way to recover your photos if you forget your password. You can find your recovery key in Settings > Account.\n\nPlease enter your recovery key here to verify that you have saved it correctly.",
                         style: enteTheme.textTheme.small
                             .copyWith(color: enteTheme.colorScheme.textMuted),
                       ),
@@ -187,12 +185,6 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                         },
                       ),
                       const SizedBox(height: 12),
-                      Text(
-                        "If you saved the recovery key from older app versions, you might have a 64 character recovery code instead of 24 words. You can enter that too.",
-                        style: enteTheme.textTheme.mini
-                            .copyWith(color: enteTheme.colorScheme.textMuted),
-                      ),
-                      const SizedBox(height: 8),
                       Expanded(
                         child: Container(
                           alignment: Alignment.bottomCenter,
@@ -204,8 +196,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                             children: [
                               GradientButton(
                                 onTap: _verifyRecoveryKey,
-                                text: "Verify",
-                                iconData: Icons.shield_outlined,
+                                text: "Confirm",
                               ),
                               const SizedBox(height: 8),
                             ],

+ 143 - 0
lib/ui/backup_settings_screen.dart

@@ -0,0 +1,143 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/ui/components/menu_section_description_widget.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/ui/components/toggle_switch_widget.dart';
+
+class BackupSettingsScreen extends StatelessWidget {
+  const BackupSettingsScreen({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          TitleBarWidget(
+            flexibleSpaceTitle: const TitleBarTitleWidget(
+              title: "Backup settings",
+            ),
+            actionIcons: [
+              IconButtonWidget(
+                icon: Icons.close_outlined,
+                iconButtonType: IconButtonType.secondary,
+                onTap: () {
+                  Navigator.pop(context);
+                  Navigator.pop(context);
+                },
+              ),
+            ],
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 16),
+                  child: Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 20),
+                    child: Column(
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        Column(
+                          children: [
+                            MenuItemWidget(
+                              captionedTextWidget: const CaptionedTextWidget(
+                                title: "Backup over mobile data",
+                              ),
+                              menuItemColor: colorScheme.fillFaint,
+                              trailingSwitch: ToggleSwitchWidget(
+                                value: () {
+                                  return Configuration.instance
+                                      .shouldBackupOverMobileData();
+                                },
+                                onChanged: () async {
+                                  await Configuration.instance
+                                      .setBackupOverMobileData(
+                                    !Configuration.instance
+                                        .shouldBackupOverMobileData(),
+                                  );
+                                },
+                              ),
+                              borderRadius: 8,
+                              alignCaptionedTextToLeft: true,
+                              isBottomBorderRadiusRemoved: true,
+                              isGestureDetectorDisabled: true,
+                            ),
+                            DividerWidget(
+                              dividerType: DividerType.menuNoIcon,
+                              bgColor: colorScheme.fillFaint,
+                            ),
+                            MenuItemWidget(
+                              captionedTextWidget: const CaptionedTextWidget(
+                                title: "Backup videos",
+                              ),
+                              menuItemColor: colorScheme.fillFaint,
+                              trailingSwitch: ToggleSwitchWidget(
+                                value: () =>
+                                    Configuration.instance.shouldBackupVideos(),
+                                onChanged: () => Configuration.instance
+                                    .setShouldBackupVideos(
+                                  !Configuration.instance.shouldBackupVideos(),
+                                ),
+                              ),
+                              borderRadius: 8,
+                              alignCaptionedTextToLeft: true,
+                              isTopBorderRadiusRemoved: true,
+                              isGestureDetectorDisabled: true,
+                            ),
+                          ],
+                        ),
+                        const SizedBox(height: 24),
+                        Platform.isIOS
+                            ? Column(
+                                children: [
+                                  MenuItemWidget(
+                                    captionedTextWidget:
+                                        const CaptionedTextWidget(
+                                      title: "Disable auto lock",
+                                    ),
+                                    menuItemColor: colorScheme.fillFaint,
+                                    trailingSwitch: ToggleSwitchWidget(
+                                      value: () => Configuration.instance
+                                          .shouldKeepDeviceAwake(),
+                                      onChanged: () {
+                                        return Configuration.instance
+                                            .setShouldKeepDeviceAwake(
+                                          !Configuration.instance
+                                              .shouldKeepDeviceAwake(),
+                                        );
+                                      },
+                                    ),
+                                    borderRadius: 8,
+                                    alignCaptionedTextToLeft: true,
+                                    isGestureDetectorDisabled: true,
+                                  ),
+                                  const MenuSectionDescriptionWidget(
+                                    content:
+                                        "Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster.",
+                                  )
+                                ],
+                              )
+                            : const SizedBox.shrink(),
+                      ],
+                    ),
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 104 - 0
lib/ui/collections/archived_collections_button_widget.dart

@@ -0,0 +1,104 @@
+// @dart=2.9
+
+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 ArchivedCollectionsButtonWidget extends StatelessWidget {
+  final TextStyle textStyle;
+
+  const ArchivedCollectionsButtonWidget(
+    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.archive_outlined,
+                    color: Theme.of(context).iconTheme.color,
+                  ),
+                  const Padding(padding: EdgeInsets.all(6)),
+                  FutureBuilder<int>(
+                    future: FilesDB.instance.fileCountWithVisibility(
+                      visibilityArchive,
+                      Configuration.instance.getUserID(),
+                    ),
+                    builder: (context, snapshot) {
+                      if (snapshot.hasData && snapshot.data > 0) {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Archive",
+                                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: "Archive",
+                                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(),
+        );
+      },
+    );
+  }
+}

+ 1 - 1
lib/ui/collections/collection_item_widget.dart

@@ -55,7 +55,7 @@ class CollectionItem extends StatelessWidget {
               FutureBuilder<int>(
                 future: FilesDB.instance.collectionFileCount(c.collection.id),
                 builder: (context, snapshot) {
-                  if (snapshot.hasData && snapshot.data! > 0) {
+                  if (snapshot.hasData) {
                     return Text(
                       snapshot.data.toString(),
                       style: enteTextTheme.small.copyWith(

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

@@ -1,49 +0,0 @@
-// @dart=2.9
-
-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,
-                    ),
-                  ),
-                ],
-              ),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-}

+ 29 - 43
lib/ui/collections/hidden_collections_button_widget.dart

@@ -1,10 +1,8 @@
 // @dart=2.9
 
 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/services/local_authentication_service.dart';
+import 'package:photos/ui/viewer/gallery/hidden_page.dart';
 import 'package:photos/utils/navigation_util.dart';
 
 class HiddenCollectionsButtonWidget extends StatelessWidget {
@@ -44,44 +42,25 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
                     color: Theme.of(context).iconTheme.color,
                   ),
                   const Padding(padding: EdgeInsets.all(6)),
-                  FutureBuilder<int>(
-                    future: FilesDB.instance.fileCountWithVisibility(
-                      visibilityArchive,
-                      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
-                            ],
+                  RichText(
+                    text: TextSpan(
+                      style: textStyle,
+                      children: [
+                        TextSpan(
+                          text: "Hidden",
+                          style: Theme.of(context).textTheme.subtitle1,
+                        ),
+                        const TextSpan(text: "  \u2022  "),
+                        WidgetSpan(
+                          child: Icon(
+                            Icons.lock_outline,
+                            size: 16,
+                            color: Theme.of(context).iconTheme.color,
                           ),
-                        );
-                      }
-                    },
+                        ),
+                        //need to query in db and bring this value
+                      ],
+                    ),
                   ),
                 ],
               ),
@@ -94,10 +73,17 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
         ),
       ),
       onPressed: () async {
-        routeToPage(
+        final hasAuthenticated = await LocalAuthenticationService.instance
+            .requestLocalAuthentication(
           context,
-          ArchivePage(),
+          "Please authenticate to view your hidden files",
         );
+        if (hasAuthenticated) {
+          routeToPage(
+            context,
+            HiddenPage(),
+          );
+        }
       },
     );
   }

+ 42 - 16
lib/ui/collections/section_title.dart

@@ -1,35 +1,61 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/theme/text_style.dart';
 
 class SectionTitle extends StatelessWidget {
-  final String title;
-  final Alignment alignment;
-  final double opacity;
+  final String? title;
+  final RichText? titleWithBrand;
 
-  const SectionTitle(
-    this.title, {
-    this.opacity = 0.8,
-    Key key,
-    this.alignment = Alignment.centerLeft,
+  const SectionTitle({
+    this.title,
+    this.titleWithBrand,
+    Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
+    final enteTextTheme = getEnteTextTheme(context);
+    Widget child;
+    if (titleWithBrand != null) {
+      child = titleWithBrand!;
+    } else if (title != null) {
+      child = Text(
+        title!,
+        style: enteTextTheme.largeBold,
+      );
+    } else {
+      child = const SizedBox.shrink();
+    }
     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),
-            ),
+            alignment: Alignment.centerLeft,
+            child: child,
           ),
         ],
       ),
     );
   }
 }
+
+RichText getOnEnteSection(BuildContext context) {
+  final EnteTextTheme textTheme = getEnteTextTheme(context);
+  return RichText(
+    text: TextSpan(
+      children: [
+        TextSpan(
+          text: "On ",
+          style: TextStyle(
+            fontWeight: FontWeight.w600,
+            fontFamily: 'Inter',
+            fontSize: 21,
+            color: textTheme.brandSmall.color,
+          ),
+        ),
+        TextSpan(text: "ente", style: textTheme.brandSmall),
+      ],
+    ),
+  );
+}

+ 6 - 4
lib/ui/collections_gallery_widget.dart

@@ -14,8 +14,8 @@ import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/collections/archived_collections_button_widget.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';
@@ -124,7 +124,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
         child: Column(
           children: [
             const SizedBox(height: 12),
-            const SectionTitle("On device"),
+            const SectionTitle(title: "On device"),
             const SizedBox(height: 12),
             const DeviceFoldersGridViewWidget(),
             const Padding(padding: EdgeInsets.all(4)),
@@ -133,7 +133,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               crossAxisAlignment: CrossAxisAlignment.end,
               children: [
-                const EnteSectionTitle(),
+                SectionTitle(titleWithBrand: getOnEnteSection(context)),
                 _sortMenu(),
               ],
             ),
@@ -148,9 +148,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Column(
                 children: [
-                  TrashButtonWidget(trashAndHiddenTextStyle),
+                  ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
                   const SizedBox(height: 12),
                   HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
+                  const SizedBox(height: 12),
+                  TrashButtonWidget(trashAndHiddenTextStyle),
                 ],
               ),
             ),

+ 13 - 4
lib/ui/common/loading_widget.dart

@@ -1,14 +1,23 @@
 import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
 
 class EnteLoadingWidget extends StatelessWidget {
-  const EnteLoadingWidget({Key? key}) : super(key: key);
+  final Color? color;
+  const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return Center(
-      child: SizedBox.fromSize(
-        size: const Size.square(30),
-        child: const CupertinoActivityIndicator(),
+      child: Padding(
+        padding: const EdgeInsets.all(5),
+        child: SizedBox.fromSize(
+          size: const Size.square(14),
+          child: CircularProgressIndicator(
+            strokeWidth: 2,
+            color: color ?? getEnteColorScheme(context).strokeBase,
+          ),
+        ),
       ),
     );
   }

+ 0 - 34
lib/ui/components/brand_title_widget.dart

@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';
-
-enum SizeVarient { small, medium, large }
-
-extension ExtraSizeVarient on SizeVarient {
-  double size() {
-    if (this == SizeVarient.small) {
-      return 21;
-    } else if (this == SizeVarient.medium) {
-      return 24;
-    } else if (this == SizeVarient.large) {
-      return 28;
-    }
-    return -1;
-  }
-}
-
-class BrandTitleWidget extends StatelessWidget {
-  final SizeVarient size;
-
-  const BrandTitleWidget({required this.size, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Text(
-      "ente",
-      style: TextStyle(
-        fontWeight: FontWeight.bold,
-        fontFamily: 'Montserrat',
-        fontSize: size.size(),
-      ),
-    );
-  }
-}

+ 1 - 1
lib/ui/components/captioned_text_widget.dart

@@ -23,7 +23,7 @@ class CaptionedTextWidget extends StatelessWidget {
 
     return Flexible(
       child: Padding(
-        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 2),
+        padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
         child: Row(
           children: [
             Flexible(

+ 59 - 0
lib/ui/components/divider_widget.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+enum DividerType {
+  solid,
+  menu,
+  menuNoIcon,
+  bottomBar,
+}
+
+class DividerWidget extends StatelessWidget {
+  final DividerType dividerType;
+  final Color bgColor;
+  const DividerWidget({
+    required this.dividerType,
+    this.bgColor = Colors.transparent,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final dividerColor = getEnteColorScheme(context).blurStrokeFaint;
+    if (dividerType == DividerType.solid) {
+      return Container(
+        color: getEnteColorScheme(context).strokeFaint,
+        width: double.infinity,
+        height: 1,
+      );
+    }
+    if (dividerType == DividerType.bottomBar) {
+      return Container(
+        color: dividerColor,
+        width: double.infinity,
+        height: 1,
+      );
+    }
+
+    return Row(
+      children: [
+        Container(
+          color: bgColor,
+          width: dividerType == DividerType.menu
+              ? 48
+              : dividerType == DividerType.menuNoIcon
+                  ? 16
+                  : 0,
+          height: 1,
+        ),
+        Expanded(
+          child: Container(
+            color: dividerColor,
+            height: 1,
+            width: double.infinity,
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 30 - 24
lib/ui/components/expandable_menu_item_widget.dart

@@ -44,32 +44,38 @@ class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
         MediaQuery.of(context).platformBrightness == Brightness.light
             ? enteColorScheme.backgroundElevated2
             : enteColorScheme.backgroundElevated;
-    return AnimatedContainer(
-      curve: Curves.ease,
-      duration: const Duration(milliseconds: 200),
-      decoration: BoxDecoration(
-        color: expandableController.value ? backgroundColor : null,
-        borderRadius: BorderRadius.circular(4),
-      ),
-      child: ExpandableNotifier(
-        controller: expandableController,
-        child: ScrollOnExpand(
-          child: ExpandablePanel(
-            header: MenuItemWidget(
-              captionedTextWidget: CaptionedTextWidget(
-                title: widget.title,
-                makeTextBold: true,
+    return Padding(
+      padding: EdgeInsets.only(bottom: expandableController.value ? 8 : 0),
+      child: AnimatedContainer(
+        curve: Curves.ease,
+        duration: const Duration(milliseconds: 200),
+        decoration: BoxDecoration(
+          color: expandableController.value ? backgroundColor : null,
+          borderRadius: BorderRadius.circular(4),
+        ),
+        child: ExpandableNotifier(
+          controller: expandableController,
+          child: ScrollOnExpand(
+            child: ExpandablePanel(
+              header: MenuItemWidget(
+                captionedTextWidget: CaptionedTextWidget(
+                  title: widget.title,
+                  makeTextBold: true,
+                ),
+                isExpandable: true,
+                leadingIcon: widget.leadingIcon,
+                trailingIcon: Icons.expand_more,
+                menuItemColor: enteColorScheme.fillFaint,
+                expandableController: expandableController,
+              ),
+              collapsed: const SizedBox.shrink(),
+              expanded: Padding(
+                padding: const EdgeInsets.only(bottom: 4),
+                child: widget.selectionOptionsWidget,
               ),
-              isHeaderOfExpansion: true,
-              leadingIcon: widget.leadingIcon,
-              trailingIcon: Icons.expand_more,
-              menuItemColor: enteColorScheme.fillFaint,
-              expandableController: expandableController,
+              theme: getExpandableTheme(context),
+              controller: expandableController,
             ),
-            collapsed: const SizedBox.shrink(),
-            expanded: widget.selectionOptionsWidget,
-            theme: getExpandableTheme(context),
-            controller: expandableController,
           ),
         ),
       ),

+ 18 - 26
lib/ui/components/home_header_widget.dart

@@ -1,8 +1,7 @@
-import 'dart:ui';
-
 import 'package:flutter/material.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/opened_settings_event.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/viewer/search/search_widget.dart';
 
 class HomeHeaderWidget extends StatefulWidget {
@@ -17,30 +16,23 @@ class HomeHeaderWidget extends StatefulWidget {
 class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
   @override
   Widget build(BuildContext context) {
-    final hasNotch = window.viewPadding.top > 65;
-    return Padding(
-      padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4),
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-        children: [
-          IconButton(
-            visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
-            onPressed: () {
-              Scaffold.of(context).openDrawer();
-              Bus.instance.fire(OpenedSettingsEvent());
-            },
-            splashColor: Colors.transparent,
-            icon: const Icon(
-              Icons.menu_outlined,
-            ),
-          ),
-          AnimatedSwitcher(
-            duration: const Duration(milliseconds: 250),
-            child: widget.centerWidget,
-          ),
-          const SearchIconWidget(),
-        ],
-      ),
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        IconButtonWidget(
+          iconButtonType: IconButtonType.primary,
+          icon: Icons.menu_outlined,
+          onTap: () {
+            Scaffold.of(context).openDrawer();
+            Bus.instance.fire(OpenedSettingsEvent());
+          },
+        ),
+        AnimatedSwitcher(
+          duration: const Duration(milliseconds: 250),
+          child: widget.centerWidget,
+        ),
+        const SearchIconWidget(),
+      ],
     );
   }
 }

+ 108 - 0
lib/ui/components/icon_button_widget.dart

@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+enum IconButtonType {
+  primary,
+  secondary,
+  rounded,
+}
+
+class IconButtonWidget extends StatefulWidget {
+  final IconButtonType iconButtonType;
+  final IconData icon;
+  final bool disableGestureDetector;
+  final VoidCallback? onTap;
+  final Color? defaultColor;
+  final Color? pressedColor;
+  final Color? iconColor;
+  const IconButtonWidget({
+    required this.icon,
+    required this.iconButtonType,
+    this.disableGestureDetector = false,
+    this.onTap,
+    this.defaultColor,
+    this.pressedColor,
+    this.iconColor,
+    super.key,
+  });
+
+  @override
+  State<IconButtonWidget> createState() => _IconButtonWidgetState();
+}
+
+class _IconButtonWidgetState extends State<IconButtonWidget> {
+  Color? iconStateColor;
+  @override
+  void didChangeDependencies() {
+    setState(() {
+      iconStateColor = null;
+    });
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colorTheme = getEnteColorScheme(context);
+    iconStateColor ??
+        (iconStateColor = widget.defaultColor ??
+            (widget.iconButtonType == IconButtonType.rounded
+                ? colorTheme.fillFaint
+                : null));
+    return widget.disableGestureDetector
+        ? _iconButton(colorTheme)
+        : GestureDetector(
+            onTapDown: _onTapDown,
+            onTapUp: _onTapUp,
+            onTapCancel: _onTapCancel,
+            onTap: widget.onTap,
+            child: _iconButton(colorTheme),
+          );
+  }
+
+  Widget _iconButton(EnteColorScheme colorTheme) {
+    return Padding(
+      padding: const EdgeInsets.all(4.0),
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 20),
+        padding: const EdgeInsets.all(8),
+        decoration: BoxDecoration(
+          borderRadius: BorderRadius.circular(20),
+          color: iconStateColor,
+        ),
+        child: Icon(
+          widget.icon,
+          color: widget.iconColor ??
+              (widget.iconButtonType == IconButtonType.secondary
+                  ? colorTheme.strokeMuted
+                  : colorTheme.strokeBase),
+          size: 24,
+        ),
+      ),
+    );
+  }
+
+  _onTapDown(details) {
+    final colorTheme = getEnteColorScheme(context);
+    setState(() {
+      iconStateColor = widget.pressedColor ??
+          (widget.iconButtonType == IconButtonType.rounded
+              ? colorTheme.fillMuted
+              : colorTheme.fillFaint);
+    });
+  }
+
+  _onTapUp(details) {
+    Future.delayed(const Duration(milliseconds: 100), () {
+      setState(() {
+        iconStateColor = null;
+      });
+    });
+  }
+
+  _onTapCancel() {
+    setState(() {
+      iconStateColor = null;
+    });
+  }
+}

+ 61 - 11
lib/ui/components/menu_item_widget.dart

@@ -4,11 +4,15 @@ import 'package:photos/ente_theme_data.dart';
 
 class MenuItemWidget extends StatefulWidget {
   final Widget captionedTextWidget;
-  final bool isHeaderOfExpansion;
-// leading icon can be passed without specifing size of icon, this component sets size to 20x20 irrespective of passed icon's size
+  final bool isExpandable;
+
+  /// leading icon can be passed without specifing size of icon,
+  /// this component sets size to 20x20 irrespective of passed icon's size
   final IconData? leadingIcon;
   final Color? leadingIconColor;
-// trailing icon can be passed without size as default size set by flutter is what this component expects
+
+  /// trailing icon can be passed without size as default size set by
+  /// flutter is what this component expects
   final IconData? trailingIcon;
   final Widget? trailingSwitch;
   final bool trailingIconIsMuted;
@@ -17,10 +21,16 @@ class MenuItemWidget extends StatefulWidget {
   final Color? menuItemColor;
   final bool alignCaptionedTextToLeft;
   final double borderRadius;
+  final Color? pressedColor;
   final ExpandableController? expandableController;
+  final bool isBottomBorderRadiusRemoved;
+  final bool isTopBorderRadiusRemoved;
+
+  /// disable gesture detector if not used
+  final bool isGestureDetectorDisabled;
   const MenuItemWidget({
     required this.captionedTextWidget,
-    this.isHeaderOfExpansion = false,
+    this.isExpandable = false,
     this.leadingIcon,
     this.leadingIconColor,
     this.trailingIcon,
@@ -31,7 +41,11 @@ class MenuItemWidget extends StatefulWidget {
     this.menuItemColor,
     this.alignCaptionedTextToLeft = false,
     this.borderRadius = 4.0,
+    this.pressedColor,
     this.expandableController,
+    this.isBottomBorderRadiusRemoved = false,
+    this.isTopBorderRadiusRemoved = false,
+    this.isGestureDetectorDisabled = false,
     Key? key,
   }) : super(key: key);
 
@@ -40,8 +54,10 @@ class MenuItemWidget extends StatefulWidget {
 }
 
 class _MenuItemWidgetState extends State<MenuItemWidget> {
+  Color? menuItemColor;
   @override
   void initState() {
+    menuItemColor = widget.menuItemColor;
     if (widget.expandableController != null) {
       widget.expandableController!.addListener(() {
         setState(() {});
@@ -50,6 +66,12 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
     super.initState();
   }
 
+  @override
+  void didChangeDependencies() {
+    menuItemColor = widget.menuItemColor;
+    super.didChangeDependencies();
+  }
+
   @override
   void dispose() {
     if (widget.expandableController != null) {
@@ -60,11 +82,14 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
 
   @override
   Widget build(BuildContext context) {
-    return widget.isHeaderOfExpansion
+    return widget.isExpandable || widget.isGestureDetectorDisabled
         ? menuItemWidget(context)
         : GestureDetector(
             onTap: widget.onTap,
             onDoubleTap: widget.onDoubleTap,
+            onTapDown: _onTapDown,
+            onTapUp: _onTapUp,
+            onTapCancel: _onCancel,
             child: menuItemWidget(context),
           );
   }
@@ -73,21 +98,25 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
     final borderRadius = Radius.circular(widget.borderRadius);
     final isExpanded = widget.expandableController?.value;
-    final bottomBorderRadius = isExpanded != null && isExpanded
+    final bottomBorderRadius =
+        (isExpanded != null && isExpanded) || widget.isBottomBorderRadiusRemoved
+            ? const Radius.circular(0)
+            : borderRadius;
+    final topBorderRadius = widget.isTopBorderRadiusRemoved
         ? const Radius.circular(0)
         : borderRadius;
     return AnimatedContainer(
-      duration: const Duration(milliseconds: 200),
+      duration: const Duration(milliseconds: 20),
       width: double.infinity,
-      padding: const EdgeInsets.symmetric(horizontal: 12),
+      padding: const EdgeInsets.only(left: 16, right: 12),
       decoration: BoxDecoration(
         borderRadius: BorderRadius.only(
-          topLeft: borderRadius,
-          topRight: borderRadius,
+          topLeft: topBorderRadius,
+          topRight: topBorderRadius,
           bottomLeft: bottomBorderRadius,
           bottomRight: bottomBorderRadius,
         ),
-        color: widget.menuItemColor,
+        color: menuItemColor,
       ),
       child: Row(
         mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -139,4 +168,25 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
       ),
     );
   }
+
+  void _onTapDown(details) {
+    setState(() {
+      menuItemColor = widget.pressedColor ?? widget.menuItemColor;
+    });
+  }
+
+  void _onTapUp(details) {
+    Future.delayed(
+      const Duration(milliseconds: 100),
+      () => setState(() {
+        menuItemColor = widget.menuItemColor;
+      }),
+    );
+  }
+
+  void _onCancel() {
+    setState(() {
+      menuItemColor = widget.menuItemColor;
+    });
+  }
 }

+ 20 - 0
lib/ui/components/menu_section_description_widget.dart

@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class MenuSectionDescriptionWidget extends StatelessWidget {
+  final String content;
+  const MenuSectionDescriptionWidget({required this.content, super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
+      child: Text(
+        content,
+        style: getEnteTextTheme(context)
+            .mini
+            .copyWith(color: getEnteColorScheme(context).textMuted),
+      ),
+    );
+  }
+}

+ 11 - 18
lib/ui/components/notification_warning_widget.dart

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/text_style.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
 
 class NotificationWarningWidget extends StatelessWidget {
   final IconData warningIcon;
@@ -33,8 +34,9 @@ class NotificationWarningWidget extends StatelessWidget {
               color: warning500,
             ),
             child: Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
               child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
                   Icon(
                     warningIcon,
@@ -50,23 +52,14 @@ class NotificationWarningWidget extends StatelessWidget {
                     ),
                   ),
                   const SizedBox(width: 12),
-                  ClipOval(
-                    child: Material(
-                      color: fillFaintDark,
-                      child: InkWell(
-                        splashColor: Colors.red, // Splash color
-                        onTap: onTap,
-                        child: SizedBox(
-                          width: 40,
-                          height: 40,
-                          child: Icon(
-                            actionIcon,
-                            color: Colors.white,
-                          ),
-                        ),
-                      ),
-                    ),
-                  ),
+                  IconButtonWidget(
+                    icon: actionIcon,
+                    iconButtonType: IconButtonType.rounded,
+                    iconColor: strokeBaseDark,
+                    defaultColor: fillFaintDark,
+                    pressedColor: fillMutedDark,
+                    onTap: onTap,
+                  )
                 ],
               ),
             ),

+ 55 - 0
lib/ui/components/title_bar_title_widget.dart

@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class TitleBarTitleWidget extends StatelessWidget {
+  final String? title;
+  final bool isTitleH2;
+  final IconData? icon;
+  const TitleBarTitleWidget({
+    this.title,
+    this.isTitleH2 = false,
+    this.icon,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context);
+    final colorTheme = getEnteColorScheme(context);
+    if (title != null) {
+      if (icon != null) {
+        return Row(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: <Widget>[
+            Text(
+              title!,
+              style: textTheme.h3Bold,
+              overflow: TextOverflow.ellipsis,
+              maxLines: 1,
+            ),
+            const SizedBox(width: 8),
+            Icon(icon, size: 20, color: colorTheme.strokeMuted),
+          ],
+        );
+      }
+      if (isTitleH2) {
+        return Text(
+          title!,
+          style: textTheme.h2Bold,
+          overflow: TextOverflow.ellipsis,
+          maxLines: 1,
+        );
+      } else {
+        return Text(
+          title!,
+          style: textTheme.h3Bold,
+          overflow: TextOverflow.ellipsis,
+          maxLines: 1,
+        );
+      }
+    }
+
+    return const SizedBox.shrink();
+  }
+}

+ 149 - 0
lib/ui/components/title_bar_widget.dart

@@ -0,0 +1,149 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+
+class TitleBarWidget extends StatelessWidget {
+  final IconButtonWidget? leading;
+  final String? title;
+  final String? caption;
+  final Widget? flexibleSpaceTitle;
+  final String? flexibleSpaceCaption;
+  final List<Widget>? actionIcons;
+  final bool isTitleH2WithoutLeading;
+  final bool isFlexibleSpaceDisabled;
+  final bool isOnTopOfScreen;
+  const TitleBarWidget({
+    this.leading,
+    this.title,
+    this.caption,
+    this.flexibleSpaceTitle,
+    this.flexibleSpaceCaption,
+    this.actionIcons,
+    this.isTitleH2WithoutLeading = false,
+    this.isFlexibleSpaceDisabled = false,
+    this.isOnTopOfScreen = true,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    const toolbarHeight = 48.0;
+    final textTheme = getEnteTextTheme(context);
+    final colorTheme = getEnteColorScheme(context);
+    return SliverAppBar(
+      primary: isOnTopOfScreen ? true : false,
+      toolbarHeight: toolbarHeight,
+      leadingWidth: 48,
+      automaticallyImplyLeading: false,
+      pinned: true,
+      expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102,
+      centerTitle: false,
+      titleSpacing: 4,
+      title: Padding(
+        padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            title == null
+                ? const SizedBox.shrink()
+                : Text(
+                    title!,
+                    style: isTitleH2WithoutLeading
+                        ? textTheme.h2Bold
+                        : textTheme.largeBold,
+                  ),
+            caption == null || isTitleH2WithoutLeading
+                ? const SizedBox.shrink()
+                : Text(
+                    caption!,
+                    style: textTheme.mini.copyWith(color: colorTheme.textMuted),
+                  )
+          ],
+        ),
+      ),
+      actions: [
+        Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 4),
+          child: Row(
+            children: _actionsWithPaddingInBetween(),
+          ),
+        ),
+      ],
+      leading: isTitleH2WithoutLeading
+          ? null
+          : leading ??
+              IconButtonWidget(
+                icon: Icons.arrow_back_outlined,
+                iconButtonType: IconButtonType.primary,
+                onTap: () {
+                  Navigator.pop(context);
+                },
+              ),
+      flexibleSpace: isFlexibleSpaceDisabled
+          ? null
+          : FlexibleSpaceBar(
+              background: SafeArea(
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  children: <Widget>[
+                    const SizedBox(height: toolbarHeight),
+                    Padding(
+                      padding: const EdgeInsets.symmetric(
+                        vertical: 4,
+                        horizontal: 16,
+                      ),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          flexibleSpaceTitle == null
+                              ? const SizedBox.shrink()
+                              : flexibleSpaceTitle!,
+                          flexibleSpaceCaption == null
+                              ? const SizedBox.shrink()
+                              : Text(
+                                  flexibleSpaceCaption!,
+                                  style: textTheme.small.copyWith(
+                                    color: colorTheme.textMuted,
+                                  ),
+                                  overflow: TextOverflow.ellipsis,
+                                  maxLines: 1,
+                                )
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+    );
+  }
+
+  _actionsWithPaddingInBetween() {
+    if (actionIcons == null) {
+      return <Widget>[const SizedBox.shrink()];
+    }
+    final actions = <Widget>[];
+    bool addWhiteSpace = false;
+    final length = actionIcons!.length;
+    int index = 0;
+    if (length == 0) {
+      return <Widget>[const SizedBox.shrink()];
+    }
+    if (length == 1) {
+      return actionIcons;
+    }
+    while (index < length) {
+      if (!addWhiteSpace) {
+        actions.add(actionIcons![index]);
+        index++;
+        addWhiteSpace = true;
+      } else {
+        actions.add(const SizedBox(width: 4));
+        addWhiteSpace = false;
+      }
+    }
+    return actions;
+  }
+}

+ 112 - 15
lib/ui/components/toggle_switch_widget.dart

@@ -1,10 +1,19 @@
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
+import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/utils/debouncer.dart';
 
-typedef OnChangedCallBack = void Function(bool);
+enum ExecutionState {
+  idle,
+  inProgress,
+  successful,
+}
+
+typedef OnChangedCallBack = Future<void> Function();
+typedef ValueCallBack = bool Function();
 
 class ToggleSwitchWidget extends StatefulWidget {
-  final bool value;
+  final ValueCallBack value;
   final OnChangedCallBack onChanged;
   const ToggleSwitchWidget({
     required this.value,
@@ -17,24 +26,112 @@ class ToggleSwitchWidget extends StatefulWidget {
 }
 
 class _ToggleSwitchWidgetState extends State<ToggleSwitchWidget> {
+  late bool toggleValue;
+  ExecutionState executionState = ExecutionState.idle;
+  final _debouncer = Debouncer(const Duration(milliseconds: 300));
+  @override
+  void initState() {
+    toggleValue = widget.value.call();
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
-    return Padding(
-      padding: const EdgeInsets.symmetric(vertical: 4),
-      child: SizedBox(
-        height: 30,
-        child: FittedBox(
-          fit: BoxFit.contain,
-          child: Switch.adaptive(
-            activeColor: enteColorScheme.primary400,
-            inactiveTrackColor: enteColorScheme.fillMuted,
-            materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-            value: widget.value,
-            onChanged: widget.onChanged,
+    final Widget stateIcon = _stateIcon(enteColorScheme);
+
+    return Row(
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(right: 2),
+          child: AnimatedSwitcher(
+            duration: const Duration(milliseconds: 175),
+            switchInCurve: Curves.easeInExpo,
+            switchOutCurve: Curves.easeOutExpo,
+            child: stateIcon,
           ),
         ),
-      ),
+        SizedBox(
+          height: 31,
+          child: FittedBox(
+            fit: BoxFit.contain,
+            child: Switch.adaptive(
+              activeColor: enteColorScheme.primary400,
+              inactiveTrackColor: enteColorScheme.fillMuted,
+              materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+              value: toggleValue,
+              onChanged: (negationOfToggleValue) async {
+                setState(() {
+                  toggleValue = negationOfToggleValue;
+                  //start showing inProgress statu icons if toggle takes more than debounce time
+                  _debouncer.run(
+                    () => Future(
+                      () {
+                        setState(() {
+                          executionState = ExecutionState.inProgress;
+                        });
+                      },
+                    ),
+                  );
+                });
+                final Stopwatch stopwatch = Stopwatch()..start();
+                await widget.onChanged.call();
+                //for toggle feedback on short unsuccessful onChanged
+                await _feedbackOnUnsuccessfulToggle(stopwatch);
+                //debouncer gets canceled if onChanged takes less than debounce time
+                _debouncer.cancelDebounce();
+                setState(() {
+                  final newValue = widget.value.call();
+                  //if onchanged on toggle is successful
+                  if (toggleValue == newValue) {
+                    if (executionState == ExecutionState.inProgress) {
+                      executionState = ExecutionState.successful;
+                      Future.delayed(const Duration(seconds: 2), () {
+                        setState(() {
+                          executionState = ExecutionState.idle;
+                        });
+                      });
+                    }
+                  } else {
+                    toggleValue = !toggleValue;
+                    executionState = ExecutionState.idle;
+                  }
+                });
+              },
+            ),
+          ),
+        ),
+      ],
     );
   }
+
+  Widget _stateIcon(enteColorScheme) {
+    if (executionState == ExecutionState.idle) {
+      return const SizedBox(width: 24);
+    } else if (executionState == ExecutionState.inProgress) {
+      return EnteLoadingWidget(
+        color: enteColorScheme.strokeMuted,
+      );
+    } else if (executionState == ExecutionState.successful) {
+      return Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 1),
+        child: Icon(
+          Icons.check_outlined,
+          size: 22,
+          color: enteColorScheme.primary500,
+        ),
+      );
+    } else {
+      return const SizedBox(width: 24);
+    }
+  }
+
+  Future<void> _feedbackOnUnsuccessfulToggle(Stopwatch stopwatch) async {
+    final timeElapsed = stopwatch.elapsedMilliseconds;
+    if (timeElapsed < 200) {
+      await Future.delayed(
+        Duration(milliseconds: 200 - timeElapsed),
+      );
+    }
+  }
 }

+ 21 - 4
lib/ui/create_collection_page.dart

@@ -3,6 +3,7 @@
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/collection.dart';
@@ -23,7 +24,7 @@ import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 
-enum CollectionActionType { addFiles, moveFiles, restoreFiles }
+enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
 
 String _actionName(CollectionActionType type, bool plural) {
   final titleSuffix = (plural ? "s" : "");
@@ -38,6 +39,9 @@ String _actionName(CollectionActionType type, bool plural) {
     case CollectionActionType.restoreFiles:
       text = "Restore file";
       break;
+    case CollectionActionType.unHide:
+      text = "Unhide file";
+      break;
   }
   return text + titleSuffix;
 }
@@ -189,8 +193,16 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   }
 
   Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
-    final List<CollectionWithThumbnail> collectionsWithThumbnail =
-        await CollectionsService.instance.getCollectionsWithThumbnails();
+    final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
+    final latestCollectionFiles =
+        await CollectionsService.instance.getLatestCollectionFiles();
+    for (final file in latestCollectionFiles) {
+      final c =
+          CollectionsService.instance.getCollectionByID(file.collectionID);
+      if (c.owner.id == Configuration.instance.getUserID() && !c.isHidden()) {
+        collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
+      }
+    }
     collectionsWithThumbnail.sort((first, second) {
       return compareAsciiLowerCaseNatural(
         first.collection.name ?? "",
@@ -273,6 +285,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
         return _addToCollection(collectionID);
       case CollectionActionType.moveFiles:
         return _moveFilesToCollection(collectionID);
+      case CollectionActionType.unHide:
+        return _moveFilesToCollection(collectionID);
       case CollectionActionType.restoreFiles:
         return _restoreFilesToCollection(collectionID);
     }
@@ -280,7 +294,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   }
 
   Future<bool> _moveFilesToCollection(int toCollectionID) async {
-    final dialog = createProgressDialog(context, "Moving files to album...");
+    final String message = widget.actionType == CollectionActionType.moveFiles
+        ? "Moving files to album..."
+        : "Unhiding files to album";
+    final dialog = createProgressDialog(context, message);
     await dialog.show();
     try {
       final int fromCollectionID =

+ 0 - 0
lib/ui/grant_permissions_widget.dart → lib/ui/home/grant_permissions_widget.dart


+ 0 - 0
lib/ui/header_error_widget.dart → lib/ui/home/header_error_widget.dart


+ 26 - 0
lib/ui/home/header_widget.dart

@@ -0,0 +1,26 @@
+import 'package:flutter/widgets.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/ui/home/memories_widget.dart';
+import 'package:photos/ui/home/status_bar_widget.dart';
+
+class HeaderWidget extends StatelessWidget {
+  static const _memoriesWidget = MemoriesWidget();
+  static const _statusBarWidget = StatusBarWidget();
+
+  const HeaderWidget({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    Logger("Header").info("Building header widget");
+    const list = [
+      _statusBarWidget,
+      _memoriesWidget,
+    ];
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: list,
+    );
+  }
+}

+ 189 - 0
lib/ui/home/home_bottom_nav_bar.dart

@@ -0,0 +1,189 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/events/tab_changed_event.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/nav_bar.dart';
+
+class HomeBottomNavigationBar extends StatefulWidget {
+  const HomeBottomNavigationBar(
+    this.selectedFiles, {
+    required this.selectedTabIndex,
+    Key? key,
+  }) : super(key: key);
+
+  final SelectedFiles selectedFiles;
+  final int selectedTabIndex;
+
+  @override
+  State<HomeBottomNavigationBar> createState() =>
+      _HomeBottomNavigationBarState();
+}
+
+class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
+  late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
+  int currentTabIndex = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    currentTabIndex = widget.selectedTabIndex;
+    widget.selectedFiles.addListener(() {
+      setState(() {});
+    });
+    _tabChangedEventSubscription =
+        Bus.instance.on<TabChangedEvent>().listen((event) {
+      if (event.source != TabChangedEventSource.tabBar) {
+        debugPrint(
+          '${(TabChangedEvent).toString()} index changed  from '
+          '$currentTabIndex to ${event.selectedIndex} via ${event.source}',
+        );
+        if (mounted) {
+          setState(() {
+            currentTabIndex = event.selectedIndex;
+          });
+        }
+      } else if (event.source == TabChangedEventSource.tabBar &&
+          currentTabIndex == event.selectedIndex) {
+        // user tapped on the currently selected index on the tapBar
+        Bus.instance.fire(TabDoubleTapEvent(currentTabIndex));
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    _tabChangedEventSubscription.cancel();
+    super.dispose();
+  }
+
+  void _onTabChange(int index, {String mode = 'tabChanged'}) {
+    debugPrint("_TabChanged called via method $mode");
+    Bus.instance.fire(
+      TabChangedEvent(
+        index,
+        TabChangedEventSource.tabBar,
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty;
+    final enteColorScheme = getEnteColorScheme(context);
+    final navBarBlur =
+        MediaQuery.of(context).platformBrightness == Brightness.light
+            ? blurBase
+            : blurMuted;
+
+    return AnimatedContainer(
+      duration: const Duration(milliseconds: 300),
+      curve: Curves.easeInOut,
+      height: filesAreSelected ? 0 : 56,
+      child: AnimatedOpacity(
+        duration: const Duration(milliseconds: 100),
+        opacity: filesAreSelected ? 0.0 : 1.0,
+        curve: Curves.easeIn,
+        child: IgnorePointer(
+          ignoring: filesAreSelected,
+          child: ListView(
+            physics: const NeverScrollableScrollPhysics(),
+            children: [
+              Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  ClipRRect(
+                    borderRadius: BorderRadius.circular(32),
+                    child: Container(
+                      alignment: Alignment.bottomCenter,
+                      height: 48,
+                      child: ClipRect(
+                        child: BackdropFilter(
+                          filter: ImageFilter.blur(
+                            sigmaX: navBarBlur,
+                            sigmaY: navBarBlur,
+                          ),
+                          child: GNav(
+                            curve: Curves.easeOutExpo,
+                            backgroundColor:
+                                getEnteColorScheme(context).fillMuted,
+                            mainAxisAlignment: MainAxisAlignment.center,
+                            rippleColor: Colors.white.withOpacity(0.1),
+                            activeColor: Theme.of(context)
+                                .colorScheme
+                                .gNavBarActiveColor,
+                            iconSize: 24,
+                            padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
+                            duration: const Duration(milliseconds: 200),
+                            gap: 0,
+                            tabBorderRadius: 32,
+                            tabBackgroundColor: Theme.of(context)
+                                .colorScheme
+                                .gNavBarActiveColor,
+                            haptic: false,
+                            tabs: [
+                              GButton(
+                                margin: const EdgeInsets.fromLTRB(8, 6, 10, 6),
+                                icon: Icons.home_rounded,
+                                iconColor: enteColorScheme.tabIcon,
+                                iconActiveColor: strokeBaseLight,
+                                text: '',
+                                onPressed: () {
+                                  _onTabChange(
+                                    0,
+                                    mode: "OnPressed",
+                                  ); // To take care of occasional missing events
+                                },
+                              ),
+                              GButton(
+                                margin: const EdgeInsets.fromLTRB(10, 6, 10, 6),
+                                icon: Icons.collections_rounded,
+                                iconColor: enteColorScheme.tabIcon,
+                                iconActiveColor: strokeBaseLight,
+                                text: '',
+                                onPressed: () {
+                                  _onTabChange(
+                                    1,
+                                    mode: "OnPressed",
+                                  ); // To take care of occasional missing
+                                  // events
+                                },
+                              ),
+                              GButton(
+                                margin: const EdgeInsets.fromLTRB(10, 6, 8, 6),
+                                icon: Icons.people_outlined,
+                                iconColor: enteColorScheme.tabIcon,
+                                iconActiveColor: strokeBaseLight,
+                                text: '',
+                                onPressed: () {
+                                  _onTabChange(
+                                    2,
+                                    mode: "OnPressed",
+                                  ); // To take care
+                                  // of occasional missing events
+                                },
+                              ),
+                            ],
+                            selectedIndex: currentTabIndex,
+                            onTabChange: _onTabChange,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 88 - 0
lib/ui/home/home_gallery_widget.dart

@@ -0,0 +1,88 @@
+// @dart=2.9
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/events/backup_folders_updated_event.dart';
+import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/events/force_reload_home_gallery_event.dart';
+import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/models/file_load_result.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/ignored_files_service.dart';
+import 'package:photos/ui/viewer/gallery/gallery.dart';
+
+class HomeGalleryWidget extends StatelessWidget {
+  final Widget header;
+  final Widget footer;
+  final SelectedFiles selectedFiles;
+
+  const HomeGalleryWidget({
+    Key key,
+    this.header,
+    this.footer,
+    this.selectedFiles,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final double bottomSafeArea = MediaQuery.of(context).padding.bottom;
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
+        final ownerID = Configuration.instance.getUserID();
+        final hasSelectedAllForBackup =
+            Configuration.instance.hasSelectedAllFoldersForBackup();
+        final collectionsToHide =
+            CollectionsService.instance.collectionsHiddenFromTimeline();
+        FileLoadResult result;
+        if (hasSelectedAllForBackup) {
+          result = await FilesDB.instance.getAllLocalAndUploadedFiles(
+            creationStartTime,
+            creationEndTime,
+            ownerID,
+            limit: limit,
+            asc: asc,
+            ignoredCollectionIDs: collectionsToHide,
+          );
+        } else {
+          result = await FilesDB.instance.getAllPendingOrUploadedFiles(
+            creationStartTime,
+            creationEndTime,
+            ownerID,
+            limit: limit,
+            asc: asc,
+            ignoredCollectionIDs: collectionsToHide,
+          );
+        }
+
+        // hide ignored files from home page UI
+        final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
+        result.files.removeWhere(
+          (f) =>
+              f.uploadedFileID == null &&
+              IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
+        );
+        return result;
+      },
+      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
+      removalEventTypes: const {
+        EventType.deletedFromRemote,
+        EventType.deletedFromEverywhere,
+        EventType.archived,
+        EventType.hide,
+      },
+      forceReloadEvents: [
+        Bus.instance.on<BackupFoldersUpdatedEvent>(),
+        Bus.instance.on<ForceReloadHomeGalleryEvent>(),
+      ],
+      tagPrefix: "home_gallery",
+      selectedFiles: selectedFiles,
+      header: header,
+      footer: footer,
+      // scrollSafe area -> SafeArea + Preserver more + Nav Bar buttons
+      scrollBottomSafeArea: bottomSafeArea + 180,
+    );
+    return gallery;
+  }
+}

+ 0 - 0
lib/ui/landing_page_widget.dart → lib/ui/home/landing_page_widget.dart


+ 5 - 3
lib/ui/memories_widget.dart → lib/ui/home/memories_widget.dart

@@ -410,9 +410,11 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
       extents: 1,
       onPageChanged: (index) async {
         await MemoriesService.instance.markMemoryAsSeen(widget.memories[index]);
-        setState(() {
-          _index = index;
-        });
+        if (mounted) {
+          setState(() {
+            _index = index;
+          });
+        }
       },
       physics: _shouldDisableScroll
           ? const NeverScrollableScrollPhysics()

+ 2 - 4
lib/ui/viewer/gallery/gallery_footer_widget.dart → lib/ui/home/preserve_footer_widget.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/services/local_sync_service.dart';
@@ -7,8 +5,8 @@ import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/utils/navigation_util.dart';
 
-class GalleryFooterWidget extends StatelessWidget {
-  const GalleryFooterWidget({Key key}) : super(key: key);
+class PreserveFooterWidget extends StatelessWidget {
+  const PreserveFooterWidget({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {

+ 63 - 0
lib/ui/home/start_backup_hook_widget.dart

@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'package:photos/services/local_sync_service.dart';
+import 'package:photos/ui/backup_folder_selection_page.dart';
+import 'package:photos/ui/common/gradient_button.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class StartBackupHookWidget extends StatelessWidget {
+  final Widget headerWidget;
+
+  const StartBackupHookWidget({super.key, required this.headerWidget});
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        headerWidget,
+        Padding(
+          padding: const EdgeInsets.only(top: 64),
+          child: Image.asset(
+            "assets/onboarding_safe.png",
+            height: 206,
+          ),
+        ),
+        Text(
+          'No photos are being backed up right now',
+          style: Theme.of(context)
+              .textTheme
+              .caption!
+              .copyWith(fontFamily: 'Inter-Medium', fontSize: 16),
+        ),
+        Center(
+          child: Material(
+            type: MaterialType.transparency,
+            child: Container(
+              width: double.infinity,
+              height: 64,
+              padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+              child: GradientButton(
+                onTap: () async {
+                  if (LocalSyncService.instance
+                      .hasGrantedLimitedPermissions()) {
+                    PhotoManager.presentLimited();
+                  } else {
+                    routeToPage(
+                      context,
+                      const BackupFolderSelectionPage(
+                        buttonText: "Start backup",
+                      ),
+                    );
+                  }
+                },
+                text: "Start backup",
+              ),
+            ),
+          ),
+        ),
+        const Padding(padding: EdgeInsets.all(50)),
+      ],
+    );
+  }
+}

+ 6 - 6
lib/ui/status_bar_widget.dart → lib/ui/home/status_bar_widget.dart

@@ -9,11 +9,11 @@ import 'package:photos/events/notification_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/user_remote_flag_service.dart';
+import 'package:photos/theme/text_style.dart';
 import 'package:photos/ui/account/verify_recovery_page.dart';
-import 'package:photos/ui/components/brand_title_widget.dart';
 import 'package:photos/ui/components/home_header_widget.dart';
 import 'package:photos/ui/components/notification_warning_widget.dart';
-import 'package:photos/ui/header_error_widget.dart';
+import 'package:photos/ui/home/header_error_widget.dart';
 import 'package:photos/utils/navigation_util.dart';
 
 const double kContainerHeight = 36;
@@ -84,9 +84,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
         HomeHeaderWidget(
           centerWidget: _showStatus
               ? _showErrorBanner
-                  ? const BrandTitleWidget(size: SizeVarient.medium)
+                  ? const Text("ente", style: brandStyleMedium)
                   : const SyncStatusWidget()
-              : const BrandTitleWidget(size: SizeVarient.medium),
+              : const Text("ente", style: brandStyleMedium),
         ),
         AnimatedOpacity(
           opacity: _showErrorBanner ? 1 : 0,
@@ -100,9 +100,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
             : const SizedBox.shrink(),
         UserRemoteFlagService.instance.shouldShowRecoveryVerification()
             ? NotificationWarningWidget(
-                warningIcon: Icons.gpp_maybe,
+                warningIcon: Icons.error_outline,
                 actionIcon: Icons.arrow_forward,
-                text: "Please ensure you have your 24 word recovery key",
+                text: "Confirm your recovery key",
                 onTap: () async => {
                   await routeToPage(
                     context,

+ 51 - 402
lib/ui/home_widget.dart

@@ -2,62 +2,48 @@
 
 import 'dart:async';
 import 'dart:io';
-import 'dart:ui';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
 import 'package:move_to_background/move_to_background.dart';
-import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.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/account_configured_event.dart';
 import 'package:photos/events/backup_folders_updated_event.dart';
-import 'package:photos/events/files_updated_event.dart';
-import 'package:photos/events/force_reload_home_gallery_event.dart';
-import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/permission_granted_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/events/trigger_logout_event.dart';
 import 'package:photos/events/user_logged_out_event.dart';
-import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
-import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.dart';
-import 'package:photos/theme/colors.dart';
-import 'package:photos/theme/effects.dart';
 import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/collections_gallery_widget.dart';
 import 'package:photos/ui/common/bottom_shadow.dart';
-import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/extents_page_view.dart';
-import 'package:photos/ui/grant_permissions_widget.dart';
-import 'package:photos/ui/landing_page_widget.dart';
+import 'package:photos/ui/home/grant_permissions_widget.dart';
+import 'package:photos/ui/home/header_widget.dart';
+import 'package:photos/ui/home/home_bottom_nav_bar.dart';
+import 'package:photos/ui/home/home_gallery_widget.dart';
+import 'package:photos/ui/home/landing_page_widget.dart';
+import 'package:photos/ui/home/preserve_footer_widget.dart';
+import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
-import 'package:photos/ui/memories_widget.dart';
-import 'package:photos/ui/nav_bar.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
 import 'package:photos/ui/shared_collections_gallery.dart';
-import 'package:photos/ui/status_bar_widget.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_footer_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
-import 'package:photos/utils/navigation_util.dart';
 import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 import 'package:uni_links/uni_links.dart';
 
@@ -81,7 +67,6 @@ class _HomeWidgetState extends State<HomeWidget> {
 
   final PageController _pageController = PageController();
   int _selectedTabIndex = 0;
-  Widget _headerWidgetWithSettingsButton;
 
   // for receiving media files
   // ignore: unused_field
@@ -100,15 +85,14 @@ class _HomeWidgetState extends State<HomeWidget> {
   @override
   void initState() {
     _logger.info("Building initstate");
-    _headerWidgetWithSettingsButton = Stack(
-      children: const [
-        _headerWidget,
-      ],
-    );
     _tabChangedEventSubscription =
         Bus.instance.on<TabChangedEvent>().listen((event) {
       if (event.source != TabChangedEventSource.pageView) {
+        debugPrint(
+          "TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}",
+        );
         _selectedTabIndex = event.selectedIndex;
+        // _pageController.jumpToPage(_selectedTabIndex);
         _pageController.animateToPage(
           event.selectedIndex,
           duration: const Duration(milliseconds: 100),
@@ -126,34 +110,7 @@ class _HomeWidgetState extends State<HomeWidget> {
     });
     _triggerLogoutEvent =
         Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
-      final AlertDialog alert = AlertDialog(
-        title: const Text("Session expired"),
-        content: const Text("Please login again"),
-        actions: [
-          TextButton(
-            child: Text(
-              "Ok",
-              style: TextStyle(
-                color: Theme.of(context).colorScheme.greenAlternative,
-              ),
-            ),
-            onPressed: () async {
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-              final dialog = createProgressDialog(context, "Logging out...");
-              await dialog.show();
-              await Configuration.instance.logout();
-              await dialog.hide();
-            },
-          ),
-        ],
-      );
-
-      showDialog(
-        context: context,
-        builder: (BuildContext context) {
-          return alert;
-        },
-      );
+      await _autoLogoutAlert();
     });
     _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
       _logger.info('logged out, selectTab index to 0');
@@ -218,6 +175,37 @@ class _HomeWidgetState extends State<HomeWidget> {
     super.initState();
   }
 
+  Future<void> _autoLogoutAlert() async {
+    final AlertDialog alert = AlertDialog(
+      title: const Text("Session expired"),
+      content: const Text("Please login again"),
+      actions: [
+        TextButton(
+          child: Text(
+            "Ok",
+            style: TextStyle(
+              color: Theme.of(context).colorScheme.greenAlternative,
+            ),
+          ),
+          onPressed: () async {
+            Navigator.of(context, rootNavigator: true).pop('dialog');
+            final dialog = createProgressDialog(context, "Logging out...");
+            await dialog.show();
+            await Configuration.instance.logout();
+            await dialog.hide();
+          },
+        ),
+      ],
+    );
+
+    await showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return alert;
+      },
+    );
+  }
+
   @override
   void dispose() {
     _tabChangedEventSubscription.cancel();
@@ -262,8 +250,8 @@ class _HomeWidgetState extends State<HomeWidget> {
       child: WillPopScope(
         child: Scaffold(
           drawerScrimColor: getEnteColorScheme(context).strokeFainter,
-          drawerEnableOpenDragGesture:
-              false, //using a hack instead of enabling this as enabling this will create other problems
+          drawerEnableOpenDragGesture: false,
+          //using a hack instead of enabling this as enabling this will create other problems
           drawer: enableDrawer
               ? ConstrainedBox(
                   constraints: const BoxConstraints(maxWidth: 428),
@@ -345,8 +333,12 @@ class _HomeWidgetState extends State<HomeWidget> {
               physics: const BouncingScrollPhysics(),
               children: [
                 showBackupFolderHook
-                    ? _getBackupFolderSelectionHook()
-                    : _getMainGalleryWidget(),
+                    ? const StartBackupHookWidget(headerWidget: _headerWidget)
+                    : HomeGalleryWidget(
+                        header: _headerWidget,
+                        footer: const PreserveFooterWidget(),
+                        selectedFiles: _selectedFiles,
+                      ),
                 _deviceFolderGalleryWidget,
                 _sharedCollectionGallery,
               ],
@@ -422,347 +414,4 @@ class _HomeWidgetState extends State<HomeWidget> {
     final ott = Uri.parse(link).queryParameters["ott"];
     UserService.instance.verifyEmail(context, ott);
   }
-
-  Widget _getMainGalleryWidget() {
-    Widget header;
-    if (_selectedFiles.files.isEmpty) {
-      header = _headerWidgetWithSettingsButton;
-    } else {
-      header = _headerWidget;
-    }
-    final gallery = Gallery(
-      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
-        final ownerID = Configuration.instance.getUserID();
-        final hasSelectedAllForBackup =
-            Configuration.instance.hasSelectedAllFoldersForBackup();
-        final archivedCollectionIds =
-            CollectionsService.instance.getArchivedCollections();
-        FileLoadResult result;
-        if (hasSelectedAllForBackup) {
-          result = await FilesDB.instance.getAllLocalAndUploadedFiles(
-            creationStartTime,
-            creationEndTime,
-            ownerID,
-            limit: limit,
-            asc: asc,
-            ignoredCollectionIDs: archivedCollectionIds,
-          );
-        } else {
-          result = await FilesDB.instance.getAllPendingOrUploadedFiles(
-            creationStartTime,
-            creationEndTime,
-            ownerID,
-            limit: limit,
-            asc: asc,
-            ignoredCollectionIDs: archivedCollectionIds,
-          );
-        }
-
-        // hide ignored files from home page UI
-        final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
-        result.files.removeWhere(
-          (f) =>
-              f.uploadedFileID == null &&
-              IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
-        );
-        return result;
-      },
-      reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
-      removalEventTypes: const {
-        EventType.deletedFromRemote,
-        EventType.deletedFromEverywhere,
-        EventType.archived,
-      },
-      forceReloadEvents: [
-        Bus.instance.on<BackupFoldersUpdatedEvent>(),
-        Bus.instance.on<ForceReloadHomeGalleryEvent>(),
-      ],
-      tagPrefix: "home_gallery",
-      selectedFiles: _selectedFiles,
-      header: header,
-      footer: const GalleryFooterWidget(),
-    );
-    return Stack(
-      children: [
-        Container(
-          child: gallery,
-        ),
-        HomePageAppBar(_selectedFiles),
-      ],
-    );
-  }
-
-  Widget _getBackupFolderSelectionHook() {
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-      children: [
-        _headerWidgetWithSettingsButton,
-        Padding(
-          padding: const EdgeInsets.only(top: 64),
-          child: Image.asset(
-            "assets/onboarding_safe.png",
-            height: 206,
-          ),
-        ),
-        Text(
-          'No photos are being backed up right now',
-          style: Theme.of(context)
-              .textTheme
-              .caption
-              .copyWith(fontFamily: 'Inter-Medium', fontSize: 16),
-        ),
-        Center(
-          child: Material(
-            type: MaterialType.transparency,
-            child: Container(
-              width: double.infinity,
-              height: 64,
-              padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
-              child: GradientButton(
-                onTap: () async {
-                  if (LocalSyncService.instance
-                      .hasGrantedLimitedPermissions()) {
-                    PhotoManager.presentLimited();
-                  } else {
-                    routeToPage(
-                      context,
-                      const BackupFolderSelectionPage(
-                        buttonText: "Start backup",
-                      ),
-                    );
-                  }
-                },
-                text: "Start backup",
-              ),
-            ),
-          ),
-        ),
-        const Padding(padding: EdgeInsets.all(50)),
-      ],
-    );
-  }
-}
-
-class HomePageAppBar extends StatefulWidget {
-  const HomePageAppBar(
-    this.selectedFiles, {
-    Key key,
-  }) : super(key: key);
-
-  final SelectedFiles selectedFiles;
-
-  @override
-  State<HomePageAppBar> createState() => _HomePageAppBarState();
-}
-
-class _HomePageAppBarState extends State<HomePageAppBar> {
-  @override
-  void initState() {
-    super.initState();
-    widget.selectedFiles.addListener(() {
-      setState(() {});
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final appBar = SizedBox(
-      height: 60,
-      child: GalleryAppBarWidget(
-        GalleryType.homepage,
-        null,
-        widget.selectedFiles,
-      ),
-    );
-    if (widget.selectedFiles.files.isEmpty) {
-      return IgnorePointer(child: appBar);
-    } else {
-      return appBar;
-    }
-  }
-}
-
-class HomeBottomNavigationBar extends StatefulWidget {
-  const HomeBottomNavigationBar(
-    this.selectedFiles, {
-    this.selectedTabIndex,
-    Key key,
-  }) : super(key: key);
-
-  final SelectedFiles selectedFiles;
-  final int selectedTabIndex;
-
-  @override
-  State<HomeBottomNavigationBar> createState() =>
-      _HomeBottomNavigationBarState();
-}
-
-class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
-  StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
-  int currentTabIndex = 0;
-
-  @override
-  void initState() {
-    super.initState();
-    currentTabIndex = widget.selectedTabIndex;
-    widget.selectedFiles.addListener(() {
-      setState(() {});
-    });
-    _tabChangedEventSubscription =
-        Bus.instance.on<TabChangedEvent>().listen((event) {
-      if (event.source != TabChangedEventSource.tabBar) {
-        debugPrint('index changed to ${event.selectedIndex}');
-        if (mounted) {
-          setState(() {
-            currentTabIndex = event.selectedIndex;
-          });
-        }
-      }
-    });
-  }
-
-  @override
-  void dispose() {
-    _tabChangedEventSubscription.cancel();
-    super.dispose();
-  }
-
-  void _onTabChange(int index) {
-    Bus.instance.fire(
-      TabChangedEvent(
-        index,
-        TabChangedEventSource.tabBar,
-      ),
-    );
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty;
-    final enteColorScheme = getEnteColorScheme(context);
-    final navBarBlur =
-        MediaQuery.of(context).platformBrightness == Brightness.light
-            ? blurBase
-            : blurMuted;
-
-    return AnimatedContainer(
-      duration: const Duration(milliseconds: 300),
-      curve: Curves.easeInOut,
-      height: filesAreSelected ? 0 : 56,
-      child: AnimatedOpacity(
-        duration: const Duration(milliseconds: 100),
-        opacity: filesAreSelected ? 0.0 : 1.0,
-        curve: Curves.easeIn,
-        child: IgnorePointer(
-          ignoring: filesAreSelected,
-          child: ListView(
-            physics: const NeverScrollableScrollPhysics(),
-            children: [
-              Row(
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: [
-                  ClipRRect(
-                    borderRadius: BorderRadius.circular(32),
-                    child: Container(
-                      alignment: Alignment.bottomCenter,
-                      height: 48,
-                      child: ClipRect(
-                        child: BackdropFilter(
-                          filter: ImageFilter.blur(
-                            sigmaX: navBarBlur,
-                            sigmaY: navBarBlur,
-                          ),
-                          child: GNav(
-                            curve: Curves.easeOutExpo,
-                            backgroundColor:
-                                getEnteColorScheme(context).fillMuted,
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            rippleColor: Colors.white.withOpacity(0.1),
-                            activeColor: Theme.of(context)
-                                .colorScheme
-                                .gNavBarActiveColor,
-                            iconSize: 24,
-                            padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
-                            duration: const Duration(milliseconds: 200),
-                            gap: 0,
-                            tabBorderRadius: 32,
-                            tabBackgroundColor: Theme.of(context)
-                                .colorScheme
-                                .gNavBarActiveColor,
-                            haptic: false,
-                            tabs: [
-                              GButton(
-                                margin: const EdgeInsets.fromLTRB(8, 6, 10, 6),
-                                icon: Icons.home_rounded,
-                                iconColor: enteColorScheme.tabIcon,
-                                iconActiveColor: strokeBaseLight,
-                                text: '',
-                                onPressed: () {
-                                  _onTabChange(
-                                    0,
-                                  ); // To take care of occasional missing events
-                                },
-                              ),
-                              GButton(
-                                margin: const EdgeInsets.fromLTRB(10, 6, 10, 6),
-                                icon: Icons.collections_rounded,
-                                iconColor: enteColorScheme.tabIcon,
-                                iconActiveColor: strokeBaseLight,
-                                text: '',
-                                onPressed: () {
-                                  _onTabChange(
-                                    1,
-                                  ); // To take care of occasional missing events
-                                },
-                              ),
-                              GButton(
-                                margin: const EdgeInsets.fromLTRB(10, 6, 8, 6),
-                                icon: Icons.people_outlined,
-                                iconColor: enteColorScheme.tabIcon,
-                                iconActiveColor: strokeBaseLight,
-                                text: '',
-                                onPressed: () {
-                                  _onTabChange(
-                                    2,
-                                  ); // To take care of occasional missing events
-                                },
-                              ),
-                            ],
-                            selectedIndex: currentTabIndex,
-                            onTabChange: _onTabChange,
-                          ),
-                        ),
-                      ),
-                    ),
-                  ),
-                ],
-              ),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-}
-
-class HeaderWidget extends StatelessWidget {
-  static const _memoriesWidget = MemoriesWidget();
-  static const _statusBarWidget = StatusBarWidget();
-
-  const HeaderWidget({
-    Key key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    Logger("Header").info("Building header widget");
-    const list = [
-      _statusBarWidget,
-      _memoriesWidget,
-    ];
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: list,
-    );
-  }
 }

+ 4 - 1
lib/ui/huge_listview/draggable_scrollbar.dart

@@ -15,6 +15,7 @@ class DraggableScrollbar extends StatefulWidget {
   final EdgeInsetsGeometry padding;
   final int totalCount;
   final int initialScrollIndex;
+  final double bottomSafeArea;
   final int currentFirstIndex;
   final ValueChanged<double> onChange;
   final String Function(int) labelTextBuilder;
@@ -26,6 +27,7 @@ class DraggableScrollbar extends StatefulWidget {
     this.backgroundColor = Colors.white,
     this.drawColor = Colors.grey,
     this.heightScrollThumb = 80.0,
+    this.bottomSafeArea = 120,
     this.padding,
     this.totalCount = 1,
     this.initialScrollIndex = 0,
@@ -49,7 +51,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
 
   double get thumbMin => 0.0;
 
-  double get thumbMax => context.size.height - widget.heightScrollThumb;
+  double get thumbMax =>
+      context.size.height - widget.heightScrollThumb - widget.bottomSafeArea;
 
   AnimationController _thumbAnimationController;
   Animation<double> _thumbAnimation;

+ 22 - 2
lib/ui/huge_listview/huge_listview.dart

@@ -38,6 +38,10 @@ class HugeListView<T> extends StatefulWidget {
   /// Height of scroll thumb, defaults to 48.
   final double thumbHeight;
 
+  /// Height of bottomSafeArea so that scroll thumb does not become hidden
+  /// or un-clickable due to footer elements. Default value is 120
+  final double bottomSafeArea;
+
   /// Called to build an individual item with the specified [index].
   final HugeListViewItemBuilder<T> itemBuilder;
 
@@ -72,6 +76,7 @@ class HugeListView<T> extends StatefulWidget {
     this.thumbBackgroundColor = Colors.red, // Colors.white,
     this.thumbDrawColor = Colors.yellow, //Colors.grey,
     this.thumbHeight = 48.0,
+    this.bottomSafeArea = 120.0,
     this.isDraggableScrollbarEnabled = true,
     this.thumbPadding,
   }) : super(key: key);
@@ -83,6 +88,7 @@ class HugeListView<T> extends StatefulWidget {
 class HugeListViewState<T> extends State<HugeListView<T>> {
   final scrollKey = GlobalKey<DraggableScrollbarState>();
   final listener = ItemPositionsListener.create();
+  int lastIndexJump = -1;
   dynamic error;
 
   @override
@@ -131,13 +137,27 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
           totalCount: widget.totalCount,
           initialScrollIndex: widget.startIndex,
           onChange: (position) {
-            widget.controller
-                ?.jumpTo(index: (position * widget.totalCount).floor());
+            final int currentIndex = _currentFirst();
+            final int floorIndex = (position * widget.totalCount).floor();
+            final int cielIndex = (position * widget.totalCount).ceil();
+            int nextIndexToJump;
+            if (floorIndex != currentIndex && floorIndex > currentIndex) {
+              nextIndexToJump = floorIndex;
+            } else if (cielIndex != currentIndex && cielIndex < currentIndex) {
+              nextIndexToJump = floorIndex;
+            } else {
+              return;
+            }
+            if (lastIndexJump != nextIndexToJump) {
+              lastIndexJump = nextIndexToJump;
+              widget.controller?.jumpTo(index: nextIndexToJump);
+            }
           },
           labelTextBuilder: widget.labelTextBuilder,
           backgroundColor: widget.thumbBackgroundColor,
           drawColor: widget.thumbDrawColor,
           heightScrollThumb: widget.thumbHeight,
+          bottomSafeArea: widget.bottomSafeArea,
           currentFirstIndex: _currentFirst(),
           isEnabled: widget.isDraggableScrollbarEnabled,
           padding: widget.thumbPadding,

+ 1 - 15
lib/ui/nav_bar.dart

@@ -2,8 +2,6 @@
 
 library google_nav_bar;
 
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -120,19 +118,7 @@ class _GNavState extends State<GNav> {
                     Colors.transparent,
                 duration: widget.duration ?? const Duration(milliseconds: 500),
                 onPressed: () {
-                  if (!clickable) return;
-                  setState(() {
-                    selectedIndex = widget.tabs.indexOf(t);
-                    clickable = false;
-                  });
-                  widget.onTabChange(selectedIndex);
-
-                  Future.delayed(
-                      widget.duration ?? const Duration(milliseconds: 500), () {
-                    setState(() {
-                      clickable = true;
-                    });
-                  });
+                  widget.onTabChange(widget.tabs.indexOf(t));
                 },
               ),
             )

+ 1 - 1
lib/ui/payment/skip_subscription_widget.dart

@@ -44,7 +44,7 @@ class SkipSubscriptionWidget extends StatelessWidget {
           BillingService.instance
               .verifySubscription(freeProductID, "", paymentProvider: "ente");
         },
-        child: const Text("Continue on free plan"),
+        child: const Text("Continue on free trial"),
       ),
     );
   }

+ 0 - 2
lib/ui/payment/stripe_subscription_page.dart

@@ -3,7 +3,6 @@
 import 'dart:async';
 
 import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/subscription.dart';
@@ -38,7 +37,6 @@ class StripeSubscriptionPage extends StatefulWidget {
 }
 
 class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
-  final _logger = Logger("StripeSubscriptionPage");
   final _billingService = BillingService.instance;
   final _userService = UserService.instance;
   Subscription _currentSubscription;

+ 1 - 1
lib/ui/payment/subscription_common_widgets.dart

@@ -94,7 +94,7 @@ class ValidityWidget extends StatelessWidget {
     );
     var message = "Renews on $endDate";
     if (currentSubscription.productID == freeProductID) {
-      message = "Free plan valid till $endDate";
+      message = "Free trial valid till $endDate";
     } else if (currentSubscription.attributes?.isCancelled ?? false) {
       message = "Your subscription will be cancelled on $endDate";
     }

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

@@ -368,7 +368,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       planWidgets.add(
         SubscriptionPlanWidget(
           storage: _freePlan.storage,
-          price: "free",
+          price: "Free trial",
           period: "",
           isActive: true,
         ),

+ 1 - 1
lib/ui/payment/subscription_plan_widget.dart

@@ -19,7 +19,7 @@ class SubscriptionPlanWidget extends StatelessWidget {
 
   String _displayPrice() {
     final result = price + (period.isNotEmpty ? " / " + period : "");
-    return result.isNotEmpty ? result : "Trial plan";
+    return price.isNotEmpty ? result : "Free trial";
   }
 
   @override

+ 4 - 0
lib/ui/settings/about_section_widget.dart

@@ -2,6 +2,7 @@
 
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/web_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@@ -47,6 +48,7 @@ class AboutSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Source code",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -61,6 +63,7 @@ class AboutSectionWidget extends StatelessWidget {
                     captionedTextWidget: const CaptionedTextWidget(
                       title: "Check for updates",
                     ),
+                    pressedColor: getEnteColorScheme(context).fillFaint,
                     trailingIcon: Icons.chevron_right_outlined,
                     trailingIconIsMuted: true,
                     onTap: () async {
@@ -111,6 +114,7 @@ class AboutMenuItemWidget extends StatelessWidget {
       captionedTextWidget: CaptionedTextWidget(
         title: title,
       ),
+      pressedColor: getEnteColorScheme(context).fillFaint,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIconIsMuted: true,
       onTap: () async {

+ 4 - 0
lib/ui/settings/account_section_widget.dart

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/account/change_email_dialog.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/recovery_key_page.dart';
@@ -34,6 +35,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Recovery key",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -67,6 +69,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Change email",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -92,6 +95,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Change password",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {

+ 0 - 1
lib/ui/settings/app_version_widget.dart

@@ -15,7 +15,6 @@ class AppVersionWidget extends StatefulWidget {
 class _AppVersionWidgetState extends State<AppVersionWidget> {
   static const kTapThresholdForInspector = 5;
   static const kConsecutiveTapTimeWindowInMilliseconds = 2000;
-  static const kDummyDelayDurationInMilliseconds = 1500;
 
   int _lastTap;
   int _consecutiveTaps = 0;

+ 16 - 53
lib/ui/settings/backup_section_widget.dart

@@ -3,18 +3,17 @@
 import 'dart:io';
 
 import 'package:flutter/material.dart';
-import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/duplicate_files.dart';
 import 'package:photos/services/deduplication_service.dart';
 import 'package:photos/services/sync_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/backup_folder_selection_page.dart';
-import 'package:photos/ui/common/dialogs.dart';
+import 'package:photos/ui/backup_settings_screen.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
-import 'package:photos/ui/components/toggle_switch_widget.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/tools/deduplicate_page.dart';
 import 'package:photos/ui/tools/free_space_page.dart';
@@ -48,6 +47,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
         captionedTextWidget: const CaptionedTextWidget(
           title: "Backed up folders",
         ),
+        pressedColor: getEnteColorScheme(context).fillFaint,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIconIsMuted: true,
         onTap: () {
@@ -62,66 +62,28 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
       sectionOptionSpacing,
       MenuItemWidget(
         captionedTextWidget: const CaptionedTextWidget(
-          title: "Backup over mobile data",
-        ),
-        trailingSwitch: ToggleSwitchWidget(
-          value: Configuration.instance.shouldBackupOverMobileData(),
-          onChanged: (value) async {
-            Configuration.instance.setBackupOverMobileData(value);
-            setState(() {});
-          },
-        ),
-      ),
-      sectionOptionSpacing,
-      MenuItemWidget(
-        captionedTextWidget: const CaptionedTextWidget(
-          title: "Backup videos",
-        ),
-        trailingSwitch: ToggleSwitchWidget(
-          value: Configuration.instance.shouldBackupVideos(),
-          onChanged: (value) async {
-            Configuration.instance.setShouldBackupVideos(value);
-            setState(() {});
-          },
+          title: "Backup settings",
         ),
+        pressedColor: getEnteColorScheme(context).fillFaint,
+        trailingIcon: Icons.chevron_right_outlined,
+        trailingIconIsMuted: true,
+        onTap: () {
+          routeToPage(
+            context,
+            const BackupSettingsScreen(),
+          );
+        },
       ),
       sectionOptionSpacing,
     ];
-    if (Platform.isIOS) {
-      sectionOptions.addAll([
-        MenuItemWidget(
-          captionedTextWidget: const CaptionedTextWidget(
-            title: "Disable auto lock",
-          ),
-          trailingSwitch: ToggleSwitchWidget(
-            value: Configuration.instance.shouldKeepDeviceAwake(),
-            onChanged: (value) async {
-              if (value) {
-                final choice = await showChoiceDialog(
-                  context,
-                  "Disable automatic screen lock when ente is running?",
-                  "This will ensure faster uploads by ensuring your device does not sleep when uploads are in progress.",
-                  firstAction: "No",
-                  secondAction: "Yes",
-                );
-                if (choice != DialogUserChoice.secondChoice) {
-                  return;
-                }
-              }
-              await Configuration.instance.setShouldKeepDeviceAwake(value);
-              setState(() {});
-            },
-          ),
-        ),
-        sectionOptionSpacing,
-      ]);
-    }
+
     sectionOptions.addAll(
       [
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Free up space",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -157,6 +119,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Deduplicate files",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {

+ 3 - 0
lib/ui/settings/danger_section_widget.dart

@@ -3,6 +3,7 @@
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/user_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/account/delete_account_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@@ -30,6 +31,7 @@ class DangerSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Logout",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () {
@@ -41,6 +43,7 @@ class DangerSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Delete account",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () {

+ 4 - 0
lib/ui/settings/debug_section_widget.dart

@@ -6,6 +6,7 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/sync_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
@@ -32,6 +33,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Key attributes",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -43,6 +45,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Delete Local Import DB",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -55,6 +58,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Allow auto-upload for ignored files",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {

+ 0 - 243
lib/ui/settings/details_section_widget.dart

@@ -1,243 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
-import 'package:photos/models/user_details.dart';
-import 'package:photos/states/user_details_state.dart';
-import 'package:photos/ui/common/loading_widget.dart';
-// ignore: import_of_legacy_library_into_null_safe
-import 'package:photos/ui/payment/subscription.dart';
-import 'package:photos/utils/data_util.dart';
-
-class DetailsSectionWidget extends StatefulWidget {
-  const DetailsSectionWidget({Key? key}) : super(key: key);
-
-  @override
-  State<DetailsSectionWidget> createState() => _DetailsSectionWidgetState();
-}
-
-class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
-  late Image _background;
-  final _logger = Logger((_DetailsSectionWidgetState).toString());
-
-  @override
-  void initState() {
-    super.initState();
-    _background = const Image(
-      image: AssetImage("assets/storage_card_background.png"),
-      fit: BoxFit.fill,
-    );
-  }
-
-  @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-    // precache background image to avoid flicker
-    // https://stackoverflow.com/questions/51343735/flutter-image-preload
-    precacheImage(_background.image, context);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final inheritedUserDetails = InheritedUserDetails.of(context);
-
-    if (inheritedUserDetails == null) {
-      _logger.severe(
-        (InheritedUserDetails).toString() +
-            ' not found before ' +
-            (_DetailsSectionWidgetState).toString() +
-            ' on tree',
-      );
-      throw Error();
-    } else {
-      return GestureDetector(
-        behavior: HitTestBehavior.translucent,
-        onTap: () async {
-          Navigator.of(context).push(
-            MaterialPageRoute(
-              builder: (BuildContext context) {
-                return getSubscriptionPage();
-              },
-            ),
-          );
-        },
-        child: containerForUserDetails(inheritedUserDetails),
-      );
-    }
-  }
-
-  Widget containerForUserDetails(
-    InheritedUserDetails inheritedUserDetails,
-  ) {
-    return ConstrainedBox(
-      constraints: const BoxConstraints(maxWidth: 428, maxHeight: 175),
-      child: Stack(
-        children: [
-          Container(
-            width: double.infinity,
-            color: Colors.transparent,
-            child: AspectRatio(
-              aspectRatio: 2 / 1,
-              child: _background,
-            ),
-          ),
-          FutureBuilder(
-            future: inheritedUserDetails.userDetails,
-            builder: (context, snapshot) {
-              if (snapshot.hasData) {
-                return userDetails(snapshot.data as UserDetails);
-              }
-              if (snapshot.hasError) {
-                _logger.severe('failed to load user details', snapshot.error);
-                return const EnteLoadingWidget();
-              }
-              return const EnteLoadingWidget();
-            },
-          ),
-          const Align(
-            alignment: Alignment.centerRight,
-            child: Icon(
-              Icons.chevron_right,
-              color: Colors.white,
-              size: 24,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  Widget userDetails(UserDetails userDetails) {
-    return Padding(
-      padding: const EdgeInsets.only(
-        top: 20,
-        bottom: 20,
-        left: 16,
-        right: 16,
-      ),
-      child: Container(
-        color: Colors.transparent,
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            Align(
-              alignment: Alignment.topLeft,
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.start,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Text(
-                    "Storage",
-                    style: Theme.of(context).textTheme.subtitle2!.copyWith(
-                          color: Colors.white.withOpacity(0.7),
-                        ),
-                  ),
-                  Text(
-                    "${convertBytesToReadableFormat(userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(userDetails.getTotalStorage())} free",
-                    style: Theme.of(context)
-                        .textTheme
-                        .headline5!
-                        .copyWith(color: Colors.white),
-                  ),
-                ],
-              ),
-            ),
-            Column(
-              mainAxisSize: MainAxisSize.max,
-              mainAxisAlignment: MainAxisAlignment.end,
-              crossAxisAlignment: CrossAxisAlignment.end,
-              children: [
-                Stack(
-                  children: <Widget>[
-                    Container(
-                      color: Colors.white.withOpacity(0.2),
-                      width: MediaQuery.of(context).size.width,
-                      height: 4,
-                    ),
-                    Container(
-                      color: Colors.white.withOpacity(0.75),
-                      width: MediaQuery.of(context).size.width *
-                          ((userDetails.getFamilyOrPersonalUsage()) /
-                              userDetails.getTotalStorage()),
-                      height: 4,
-                    ),
-                    Container(
-                      color: Colors.white,
-                      width: MediaQuery.of(context).size.width *
-                          (userDetails.usage / userDetails.getTotalStorage()),
-                      height: 4,
-                    ),
-                  ],
-                ),
-                const Padding(
-                  padding: EdgeInsets.symmetric(vertical: 8),
-                ),
-                Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                  children: [
-                    userDetails.isPartOfFamily()
-                        ? Row(
-                            children: [
-                              Container(
-                                width: 8.71,
-                                height: 8.99,
-                                decoration: const BoxDecoration(
-                                  shape: BoxShape.circle,
-                                  color: Colors.white,
-                                ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 4),
-                              ),
-                              Text(
-                                "You",
-                                style: Theme.of(context)
-                                    .textTheme
-                                    .bodyText1!
-                                    .copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 12),
-                              ),
-                              Container(
-                                width: 8.71,
-                                height: 8.99,
-                                decoration: BoxDecoration(
-                                  shape: BoxShape.circle,
-                                  color: Colors.white.withOpacity(0.75),
-                                ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 4),
-                              ),
-                              Text(
-                                "Family",
-                                style: Theme.of(context)
-                                    .textTheme
-                                    .bodyText1!
-                                    .copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
-                              ),
-                            ],
-                          )
-                        : Text(
-                            "${convertBytesToReadableFormat(userDetails.getFamilyOrPersonalUsage())} used",
-                            style:
-                                Theme.of(context).textTheme.bodyText1!.copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
-                          ),
-                  ],
-                ),
-              ],
-            )
-          ],
-        ),
-      ),
-    );
-  }
-}

+ 81 - 85
lib/ui/settings/security_section_widget.dart

@@ -11,6 +11,7 @@ import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/two_factor_status_change_event.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/account/sessions_page.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
@@ -72,8 +73,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                 ),
                 trailingSwitch: snapshot.hasData
                     ? ToggleSwitchWidget(
-                        value: snapshot.data,
-                        onChanged: (value) async {
+                        value: () => snapshot.data,
+                        onChanged: () async {
                           final hasAuthenticated =
                               await LocalAuthenticationService.instance
                                   .requestLocalAuthentication(
@@ -81,7 +82,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                             "Please authenticate to configure two-factor authentication",
                           );
                           if (hasAuthenticated) {
-                            if (value) {
+                            if (!snapshot.data) {
                               UserService.instance.setupTwoFactor(context);
                             } else {
                               _disableTwoFactor();
@@ -105,18 +106,15 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
           title: "Lockscreen",
         ),
         trailingSwitch: ToggleSwitchWidget(
-          value: _config.shouldShowLockScreen(),
-          onChanged: (value) async {
-            final hasAuthenticated = await LocalAuthenticationService.instance
+          value: () => _config.shouldShowLockScreen(),
+          onChanged: () async {
+            await LocalAuthenticationService.instance
                 .requestLocalAuthForLockScreen(
               context,
-              value,
+              !_config.shouldShowLockScreen(),
               "Please authenticate to change lockscreen setting",
               "To enable lockscreen, please setup device passcode or screen lock in your system settings.",
             );
-            if (hasAuthenticated) {
-              setState(() {});
-            }
           },
         ),
       ),
@@ -130,81 +128,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
               title: "Hide from recents",
             ),
             trailingSwitch: ToggleSwitchWidget(
-              value: _config.shouldHideFromRecents(),
-              onChanged: (value) async {
-                if (value) {
-                  final AlertDialog alert = AlertDialog(
-                    title: const Text("Hide from recents?"),
-                    content: SingleChildScrollView(
-                      child: Column(
-                        mainAxisAlignment: MainAxisAlignment.start,
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        children: const [
-                          Text(
-                            "Hiding from the task switcher will prevent you from taking screenshots in this app.",
-                            style: TextStyle(
-                              height: 1.5,
-                            ),
-                          ),
-                          Padding(padding: EdgeInsets.all(8)),
-                          Text(
-                            "Are you sure?",
-                            style: TextStyle(
-                              height: 1.5,
-                            ),
-                          ),
-                        ],
-                      ),
-                    ),
-                    actions: [
-                      TextButton(
-                        child: Text(
-                          "No",
-                          style: TextStyle(
-                            color:
-                                Theme.of(context).colorScheme.defaultTextColor,
-                          ),
-                        ),
-                        onPressed: () {
-                          Navigator.of(context, rootNavigator: true)
-                              .pop('dialog');
-                        },
-                      ),
-                      TextButton(
-                        child: Text(
-                          "Yes",
-                          style: TextStyle(
-                            color:
-                                Theme.of(context).colorScheme.defaultTextColor,
-                          ),
-                        ),
-                        onPressed: () async {
-                          Navigator.of(context, rootNavigator: true)
-                              .pop('dialog');
-                          await _config.setShouldHideFromRecents(true);
-                          await FlutterWindowManager.addFlags(
-                            FlutterWindowManager.FLAG_SECURE,
-                          );
-                          setState(() {});
-                        },
-                      ),
-                    ],
-                  );
-
-                  showDialog(
-                    context: context,
-                    builder: (BuildContext context) {
-                      return alert;
-                    },
-                  );
-                } else {
-                  await _config.setShouldHideFromRecents(false);
-                  await FlutterWindowManager.clearFlags(
-                    FlutterWindowManager.FLAG_SECURE,
-                  );
-                  setState(() {});
-                }
-              },
+              value: () => _config.shouldHideFromRecents(),
+              onChanged: _hideFromRecentsOnChanged,
             ),
           ),
           sectionOptionSpacing,
@@ -216,6 +141,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
         captionedTextWidget: const CaptionedTextWidget(
           title: "Active sessions",
         ),
+        pressedColor: getEnteColorScheme(context).fillFaint,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIconIsMuted: true,
         onTap: () async {
@@ -282,4 +208,74 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
       },
     );
   }
+
+  Future<void> _hideFromRecentsOnChanged() async {
+    if (!_config.shouldHideFromRecents()) {
+      final AlertDialog alert = AlertDialog(
+        title: const Text("Hide from recents?"),
+        content: SingleChildScrollView(
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: const [
+              Text(
+                "Hiding from the task switcher will prevent you from taking screenshots in this app.",
+                style: TextStyle(
+                  height: 1.5,
+                ),
+              ),
+              Padding(padding: EdgeInsets.all(8)),
+              Text(
+                "Are you sure?",
+                style: TextStyle(
+                  height: 1.5,
+                ),
+              ),
+            ],
+          ),
+        ),
+        actions: [
+          TextButton(
+            child: Text(
+              "No",
+              style: TextStyle(
+                color: Theme.of(context).colorScheme.defaultTextColor,
+              ),
+            ),
+            onPressed: () {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+            },
+          ),
+          TextButton(
+            child: Text(
+              "Yes",
+              style: TextStyle(
+                color: Theme.of(context).colorScheme.defaultTextColor,
+              ),
+            ),
+            onPressed: () async {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+              await _config.setShouldHideFromRecents(true);
+              await FlutterWindowManager.addFlags(
+                FlutterWindowManager.FLAG_SECURE,
+              );
+              setState(() {});
+            },
+          ),
+        ],
+      );
+
+      showDialog(
+        context: context,
+        builder: (BuildContext context) {
+          return alert;
+        },
+      );
+    } else {
+      await _config.setShouldHideFromRecents(false);
+      await FlutterWindowManager.clearFlags(
+        FlutterWindowManager.FLAG_SECURE,
+      );
+    }
+  }
 }

+ 2 - 4
lib/ui/settings/settings_title_bar_widget.dart

@@ -37,15 +37,13 @@ class SettingsTitleBarWidget extends StatelessWidget {
                         ' on tree',
                   );
                   throw Error();
-                }
-                if (snapshot.hasData) {
+                } else if (snapshot.hasData) {
                   final userDetails = snapshot.data as UserDetails;
                   return Text(
                     "${NumberFormat().format(userDetails.fileCount)} memories",
                     style: getEnteTextTheme(context).largeBold,
                   );
-                }
-                if (snapshot.hasError) {
+                } else if (snapshot.hasError) {
                   logger.severe('failed to load user details');
                   return const EnteLoadingWidget();
                 } else {

+ 2 - 0
lib/ui/settings/social_section_widget.dart

@@ -4,6 +4,7 @@ import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
@@ -61,6 +62,7 @@ class SocialsMenuItemWidget extends StatelessWidget {
       captionedTextWidget: CaptionedTextWidget(
         title: text,
       ),
+      pressedColor: getEnteColorScheme(context).fillFaint,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIconIsMuted: true,
       onTap: () {

+ 284 - 0
lib/ui/settings/storage_card_widget.dart

@@ -0,0 +1,284 @@
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/models/user_details.dart';
+import 'package:photos/states/user_details_state.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/common/loading_widget.dart';
+// ignore: import_of_legacy_library_into_null_safe
+import 'package:photos/ui/payment/subscription.dart';
+import 'package:photos/ui/settings/storage_error_widget.dart';
+import 'package:photos/ui/settings/storage_progress_widget.dart';
+import 'package:photos/utils/data_util.dart';
+
+class StorageCardWidget extends StatefulWidget {
+  const StorageCardWidget({Key? key}) : super(key: key);
+
+  @override
+  State<StorageCardWidget> createState() => _StorageCardWidgetState();
+}
+
+class _StorageCardWidgetState extends State<StorageCardWidget> {
+  late Image _background;
+  final _logger = Logger((_StorageCardWidgetState).toString());
+  final ValueNotifier<bool> _isStorageCardPressed = ValueNotifier(false);
+
+  @override
+  void initState() {
+    super.initState();
+    _background = const Image(
+      image: AssetImage("assets/storage_card_background.png"),
+      fit: BoxFit.fill,
+    );
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    // precache background image to avoid flicker
+    // https://stackoverflow.com/questions/51343735/flutter-image-preload
+    precacheImage(_background.image, context);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final inheritedUserDetails = InheritedUserDetails.of(context);
+
+    if (inheritedUserDetails == null) {
+      _logger.severe(
+        (InheritedUserDetails).toString() + 'is null',
+      );
+      throw Error();
+    } else {
+      return GestureDetector(
+        behavior: HitTestBehavior.translucent,
+        onTap: () async {
+          Navigator.of(context).push(
+            MaterialPageRoute(
+              builder: (BuildContext context) {
+                return getSubscriptionPage();
+              },
+            ),
+          );
+        },
+        onTapDown: (details) => _isStorageCardPressed.value = true,
+        onTapCancel: () => _isStorageCardPressed.value = false,
+        onTapUp: (details) => _isStorageCardPressed.value = false,
+        child: containerForUserDetails(inheritedUserDetails),
+      );
+    }
+  }
+
+  Widget containerForUserDetails(
+    InheritedUserDetails inheritedUserDetails,
+  ) {
+    return ConstrainedBox(
+      constraints: const BoxConstraints(maxWidth: 350),
+      child: AspectRatio(
+        aspectRatio: 2 / 1,
+        child: Stack(
+          children: [
+            _background,
+            FutureBuilder(
+              future: inheritedUserDetails.userDetails,
+              builder: (context, snapshot) {
+                if (snapshot.hasData) {
+                  return userDetails(snapshot.data as UserDetails);
+                }
+                if (snapshot.hasError) {
+                  _logger.severe(
+                    'failed to load user details',
+                    snapshot.error,
+                  );
+                  return const StorageErrorWidget();
+                }
+                return const EnteLoadingWidget(color: strokeBaseDark);
+              },
+            ),
+            Align(
+              alignment: Alignment.centerRight,
+              child: Padding(
+                padding: const EdgeInsets.only(right: 4),
+                child: ValueListenableBuilder<bool>(
+                  builder: (BuildContext context, bool value, Widget? child) {
+                    return Icon(
+                      Icons.chevron_right_outlined,
+                      color: value ? strokeMutedDark : strokeBaseDark,
+                    );
+                  },
+                  valueListenable: _isStorageCardPressed,
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget userDetails(UserDetails userDetails) {
+    const hundredMBinBytes = 107374182;
+
+    final isMobileScreenSmall = MediaQuery.of(context).size.width <= 365;
+    final freeSpaceInBytes = userDetails.getFreeStorage();
+    final shouldShowFreeSpaceInMBs = freeSpaceInBytes < hundredMBinBytes;
+
+    final usedSpaceInGB = roundBytesUsedToGBs(
+      userDetails.getFamilyOrPersonalUsage(),
+      userDetails.getFreeStorage(),
+    );
+    final totalStorageInGB =
+        convertBytesToGBs(userDetails.getTotalStorage()).truncate();
+
+    return Padding(
+      padding: EdgeInsets.fromLTRB(
+        16,
+        20,
+        16,
+        isMobileScreenSmall ? 12 : 20,
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Align(
+            alignment: Alignment.topLeft,
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  isMobileScreenSmall ? "Used space" : "Storage",
+                  style: getEnteTextTheme(context)
+                      .small
+                      .copyWith(color: textMutedDark),
+                ),
+                const SizedBox(height: 2),
+                RichText(
+                  overflow: TextOverflow.ellipsis,
+                  maxLines: 1,
+                  text: TextSpan(
+                    style: getEnteTextTheme(context)
+                        .h3Bold
+                        .copyWith(color: textBaseDark),
+                    children: [
+                      TextSpan(text: usedSpaceInGB.toString()),
+                      TextSpan(text: isMobileScreenSmall ? "/" : " GB of "),
+                      TextSpan(text: totalStorageInGB.toString() + " GB"),
+                      TextSpan(text: isMobileScreenSmall ? "" : " used"),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ),
+          Column(
+            children: [
+              Stack(
+                children: <Widget>[
+                  const StorageProgressWidget(
+                    color:
+                        Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma
+                    fractionOfStorage: 1,
+                  ),
+                  userDetails.isPartOfFamily()
+                      ? StorageProgressWidget(
+                          color: strokeBaseDark,
+                          fractionOfStorage:
+                              ((userDetails.getFamilyOrPersonalUsage()) /
+                                  userDetails.getTotalStorage()),
+                        )
+                      : const SizedBox.shrink(),
+                  StorageProgressWidget(
+                    color: userDetails.isPartOfFamily()
+                        ? getEnteColorScheme(context).primary300
+                        : strokeBaseDark,
+                    fractionOfStorage:
+                        (userDetails.usage / userDetails.getTotalStorage()),
+                  )
+                ],
+              ),
+              const SizedBox(height: 12),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  userDetails.isPartOfFamily()
+                      ? Row(
+                          children: [
+                            Container(
+                              width: 8.71,
+                              height: 8.99,
+                              decoration: BoxDecoration(
+                                shape: BoxShape.circle,
+                                color: getEnteColorScheme(context).primary300,
+                              ),
+                            ),
+                            const SizedBox(width: 4),
+                            Text(
+                              "You",
+                              style: getEnteTextTheme(context)
+                                  .miniBold
+                                  .copyWith(color: textBaseDark),
+                            ),
+                            const SizedBox(width: 12),
+                            Container(
+                              width: 8.71,
+                              height: 8.99,
+                              decoration: const BoxDecoration(
+                                shape: BoxShape.circle,
+                                color: textBaseDark,
+                              ),
+                            ),
+                            const SizedBox(width: 4),
+                            Text(
+                              "Family",
+                              style: getEnteTextTheme(context)
+                                  .miniBold
+                                  .copyWith(color: textBaseDark),
+                            ),
+                          ],
+                        )
+                      : const SizedBox.shrink(),
+                  RichText(
+                    text: TextSpan(
+                      style: getEnteTextTheme(context)
+                          .mini
+                          .copyWith(color: textFaintDark),
+                      children: [
+                        TextSpan(
+                          text:
+                              "${shouldShowFreeSpaceInMBs ? convertBytesToMBs(freeSpaceInBytes) : _roundedFreeSpace(totalStorageInGB, usedSpaceInGB)}",
+                        ),
+                        TextSpan(
+                          text: shouldShowFreeSpaceInMBs
+                              ? " MB free"
+                              : " GB free",
+                        )
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          )
+        ],
+      ),
+    );
+  }
+
+  num _roundedFreeSpace(num totalStorageInGB, num usedSpaceInGB) {
+    int fractionDigits;
+    //subtracting usedSpace from totalStorage in GB instead of converting from bytes so that free space and used space adds up in the UI
+    final freeSpace = totalStorageInGB - usedSpaceInGB;
+    //show one decimal place if free space is less than 10GB
+    if (freeSpace < 10) {
+      fractionDigits = 1;
+    } else {
+      fractionDigits = 0;
+    }
+    //omit decimal if decimal is 0
+    if (fractionDigits == 1 && freeSpace.remainder(1) == 0) {
+      fractionDigits = 0;
+    }
+    return num.parse(freeSpace.toStringAsFixed(fractionDigits));
+  }
+}

+ 31 - 0
lib/ui/settings/storage_error_widget.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class StorageErrorWidget extends StatelessWidget {
+  const StorageErrorWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(12),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          const Icon(
+            Icons.error_outline_outlined,
+            color: strokeBaseDark,
+          ),
+          const SizedBox(height: 8),
+          Text(
+            "Your storage details could not be fetched",
+            style: getEnteTextTheme(context).small.copyWith(
+                  color: textMutedDark,
+                ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 27 - 0
lib/ui/settings/storage_progress_widget.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+class StorageProgressWidget extends StatelessWidget {
+  final Color color;
+  final double fractionOfStorage;
+  const StorageProgressWidget({
+    required this.color,
+    required this.fractionOfStorage,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (context, constrains) {
+        return Container(
+          decoration: BoxDecoration(
+            borderRadius: BorderRadius.circular(2),
+            color: color,
+          ),
+          width: constrains.maxWidth * fractionOfStorage,
+          height: 4,
+        );
+      },
+    );
+  }
+}

+ 4 - 0
lib/ui/settings/support_section_widget.dart

@@ -5,6 +5,7 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/web_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@@ -34,6 +35,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Email",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {
@@ -45,6 +47,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Roadmap",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () {
@@ -67,6 +70,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
             title: "Report a bug",
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           onTap: () async {

+ 3 - 1
lib/ui/settings/theme_switch_widget.dart

@@ -4,6 +4,7 @@ import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:photos/ente_theme_data.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
@@ -69,7 +70,8 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
         title: toBeginningOfSentenceCase(themeMode.name),
         textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
       ),
-      isHeaderOfExpansion: false,
+      pressedColor: getEnteColorScheme(context).fillFaint,
+      isExpandable: false,
       trailingIcon: currentThemeMode == themeMode ? Icons.check : null,
       onTap: () async {
         AdaptiveTheme.of(context).setThemeMode(themeMode);

+ 3 - 2
lib/ui/settings_page.dart

@@ -14,10 +14,10 @@ import 'package:photos/ui/settings/app_version_widget.dart';
 import 'package:photos/ui/settings/backup_section_widget.dart';
 import 'package:photos/ui/settings/danger_section_widget.dart';
 import 'package:photos/ui/settings/debug_section_widget.dart';
-import 'package:photos/ui/settings/details_section_widget.dart';
 import 'package:photos/ui/settings/security_section_widget.dart';
 import 'package:photos/ui/settings/settings_title_bar_widget.dart';
 import 'package:photos/ui/settings/social_section_widget.dart';
+import 'package:photos/ui/settings/storage_card_widget.dart';
 import 'package:photos/ui/settings/support_section_widget.dart';
 import 'package:photos/ui/settings/theme_switch_widget.dart';
 
@@ -42,6 +42,7 @@ class SettingsPage extends StatelessWidget {
     final List<Widget> contents = [];
     contents.add(
       Container(
+        constraints: const BoxConstraints(maxWidth: 350),
         padding: const EdgeInsets.symmetric(horizontal: 8),
         child: Align(
           alignment: Alignment.centerLeft,
@@ -65,7 +66,7 @@ class SettingsPage extends StatelessWidget {
     contents.add(const SizedBox(height: 8));
     if (hasLoggedIn) {
       contents.addAll([
-        const DetailsSectionWidget(),
+        const StorageCardWidget(),
         const SizedBox(height: 12),
         const BackupSectionWidget(),
         sectionSpacing,

+ 2 - 2
lib/ui/shared_collections_gallery.dart

@@ -126,7 +126,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
         child: Column(
           children: [
             const SizedBox(height: 12),
-            const SectionTitle("Shared with me"),
+            const SectionTitle(title: "Shared with me"),
             const SizedBox(height: 12),
             collections.incoming.isNotEmpty
                 ? Padding(
@@ -150,7 +150,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
                     ),
                   )
                 : _getIncomingCollectionEmptyState(),
-            const SectionTitle("Shared by me"),
+            const SectionTitle(title: "Shared by me"),
             const SizedBox(height: 12),
             collections.outgoing.isNotEmpty
                 ? ListView.builder(

+ 9 - 2
lib/ui/tools/editor/image_editor_page.dart

@@ -1,6 +1,7 @@
 // @dart=2.9
 
 import 'dart:io';
+import 'dart:math';
 import 'dart:typed_data';
 
 import 'package:extended_image/extended_image.dart';
@@ -370,13 +371,19 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
         existingFiles[0].creationTime,
       ))
           .files;
+      // the index could be -1 if the files fetched doesn't contain the newly
+      // edited files
+      final selectionIndex =
+          files.indexWhere((file) => file.generatedID == newFile.generatedID);
+      if (selectionIndex == -1) {
+        files.add(newFile);
+      }
       replacePage(
         context,
         DetailPage(
           widget.detailPageConfig.copyWith(
             files: files,
-            selectedIndex: files
-                .indexWhere((file) => file.generatedID == newFile.generatedID),
+            selectedIndex: min(selectionIndex, files.length - 1),
           ),
         ),
       );

+ 11 - 5
lib/ui/viewer/file/collections_list_of_file_widget.dart

@@ -2,6 +2,7 @@
 
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
@@ -11,6 +12,7 @@ import 'package:photos/utils/navigation_util.dart';
 
 class CollectionsListOfFileWidget extends StatelessWidget {
   final Future<Set<int>> allCollectionIDsOfFile;
+
   const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
       : super(key: key);
 
@@ -21,19 +23,23 @@ class CollectionsListOfFileWidget extends StatelessWidget {
       builder: (context, snapshot) {
         if (snapshot.hasData) {
           final Set<int> collectionIDs = snapshot.data;
-          final collections = [];
+          final collections = <Collection>[];
           for (var collectionID in collectionIDs) {
-            collections.add(
-              CollectionsService.instance.getCollectionByID(collectionID),
-            );
+            final c =
+                CollectionsService.instance.getCollectionByID(collectionID);
+            collections.add(c);
           }
           return ListView.builder(
             itemCount: collections.length,
             scrollDirection: Axis.horizontal,
             itemBuilder: (context, index) {
+              final bool isHidden = collections[index].isHidden();
               return FileInfoCollectionWidget(
-                name: collections[index].name,
+                name: isHidden ? 'Hidden' : collections[index].name,
                 onTap: () {
+                  if (isHidden) {
+                    return;
+                  }
                   routeToPage(
                     context,
                     CollectionPage(

+ 166 - 35
lib/ui/viewer/file/fading_app_bar.dart

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:like_button/like_button.dart';
 import 'package:logging/logging.dart';
 import 'package:media_extension/media_extension.dart';
+import 'package:page_transition/page_transition.dart';
 import 'package:path/path.dart' as file_path;
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/event_bus.dart';
@@ -16,11 +17,15 @@ import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/ignored_file.dart';
+import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_file.dart';
+import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/favorites_service.dart';
+import 'package:photos/services/hidden_service.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
+import 'package:photos/ui/create_collection_page.dart';
 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';
@@ -99,11 +104,21 @@ class FadingAppBarState extends State<FadingAppBar> {
 
   AppBar _buildAppBar() {
     debugPrint("building app bar");
+
     final List<Widget> actions = [];
     final isTrashedFile = widget.file is TrashFile;
     final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
+    final bool isOwnedByUser =
+        widget.file.ownerID == null || widget.file.ownerID == widget.userID;
+    bool isFileHidden = false;
+    if (isOwnedByUser && widget.file.uploadedFileID != null) {
+      isFileHidden = CollectionsService.instance
+              .getCollectionByID(widget.file.collectionID)
+              ?.isHidden() ??
+          false;
+    }
     // only show fav option for files owned by the user
-    if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
+    if (isOwnedByUser && !isFileHidden) {
       actions.add(_getFavoriteButton());
     }
     actions.add(
@@ -132,8 +147,7 @@ class FadingAppBarState extends State<FadingAppBar> {
             );
           }
           // options for files owned by the user
-          if (widget.file.ownerID == null ||
-              widget.file.ownerID == widget.userID) {
+          if (isOwnedByUser) {
             items.add(
               PopupMenuItem(
                 value: 2,
@@ -169,12 +183,51 @@ class FadingAppBarState extends State<FadingAppBar> {
                     const Padding(
                       padding: EdgeInsets.all(8),
                     ),
-                    const Text("Use as"),
+                    const Text("Set as"),
                   ],
                 ),
               ),
             );
           }
+          if (isOwnedByUser) {
+            if (!isFileHidden) {
+              items.add(
+                PopupMenuItem(
+                  value: 4,
+                  child: Row(
+                    children: [
+                      Icon(
+                        Icons.visibility_off,
+                        color: Theme.of(context).iconTheme.color,
+                      ),
+                      const Padding(
+                        padding: EdgeInsets.all(8),
+                      ),
+                      const Text("Hide"),
+                    ],
+                  ),
+                ),
+              );
+            } else {
+              items.add(
+                PopupMenuItem(
+                  value: 5,
+                  child: Row(
+                    children: [
+                      Icon(
+                        Icons.visibility,
+                        color: Theme.of(context).iconTheme.color,
+                      ),
+                      const Padding(
+                        padding: EdgeInsets.all(8),
+                      ),
+                      const Text("Unhide"),
+                    ],
+                  ),
+                ),
+              );
+            }
+          }
           return items;
         },
         onSelected: (value) {
@@ -184,6 +237,10 @@ class FadingAppBarState extends State<FadingAppBar> {
             _showDeleteSheet(widget.file);
           } else if (value == 3) {
             _setAs(widget.file);
+          } else if (value == 4) {
+            _handleHideRequest(context);
+          } else if (value == 5) {
+            _handleUnHideRequest(context);
           }
         },
       ),
@@ -197,6 +254,38 @@ class FadingAppBarState extends State<FadingAppBar> {
     );
   }
 
+  Future<void> _handleHideRequest(BuildContext context) async {
+    try {
+      final hideResult =
+          await CollectionsService.instance.hideFiles(context, [widget.file]);
+
+      if (hideResult) {
+        // delay to avoid black screen
+        await Future.delayed(const Duration(milliseconds: 300));
+        Navigator.of(context).pop();
+      }
+    } catch (e, s) {
+      _logger.severe("failed to update file visibility", e, s);
+      await showGenericErrorDialog(context);
+    }
+  }
+
+  Future<void> _handleUnHideRequest(BuildContext context) async {
+    final s = SelectedFiles();
+    s.files.add(widget.file);
+    Navigator.push(
+      context,
+      PageTransition(
+        type: PageTransitionType.bottomToTop,
+        child: CreateCollectionPage(
+          s,
+          null,
+          actionType: CollectionActionType.unHide,
+        ),
+      ),
+    );
+  }
+
   Widget _getFavoriteButton() {
     return FutureBuilder(
       future: FavoritesService.instance.isFavorite(widget.file),
@@ -326,55 +415,97 @@ class FadingAppBarState extends State<FadingAppBar> {
   Future<void> _download(File file) async {
     final dialog = createProgressDialog(context, "Downloading...");
     await dialog.show();
-    final FileType type = file.fileType;
-    // save and track image for livePhoto/image and video for FileType.video
-    final io.File fileToSave = await getFile(file);
-    final savedAsset = type == FileType.video
-        ? (await PhotoManager.editor.saveVideo(fileToSave, title: file.title))
-        : (await PhotoManager.editor
-            .saveImageWithPath(fileToSave.path, title: file.title));
-    // immediately track assetID to avoid duplicate upload
-    await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
-    file.localID = savedAsset.id;
-    await FilesDB.instance.insert(file);
+    try {
+      final FileType type = file.fileType;
+      final bool downloadLivePhotoOnDroid =
+          type == FileType.livePhoto && Platform.isAndroid;
+      AssetEntity savedAsset;
+      final io.File fileToSave = await getFile(file);
+      if (type == FileType.image) {
+        savedAsset = await PhotoManager.editor
+            .saveImageWithPath(fileToSave.path, title: file.title);
+      } else if (type == FileType.video) {
+        savedAsset =
+            await PhotoManager.editor.saveVideo(fileToSave, title: file.title);
+      } else if (type == FileType.livePhoto) {
+        final io.File liveVideoFile =
+            await getFileFromServer(file, liveVideo: true);
+        if (liveVideoFile == null) {
+          throw AssertionError("Live video can not be null");
+        }
+        if (downloadLivePhotoOnDroid) {
+          await _saveLivePhotoOnDroid(fileToSave, liveVideoFile, file);
+        } else {
+          savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
+            imageFile: fileToSave,
+            videoFile: liveVideoFile,
+            title: file.title,
+          );
+        }
+      }
 
-    if (type == FileType.livePhoto) {
-      final io.File liveVideo = await getFileFromServer(file, liveVideo: true);
-      if (liveVideo == null) {
-        _logger.warning("Failed to find live video" + file.tag);
-      } else {
-        final videoTitle = file_path.basenameWithoutExtension(file.title) +
-            file_path.extension(liveVideo.path);
-        final savedAsset = (await PhotoManager.editor.saveVideo(
-          liveVideo,
-          title: videoTitle,
-        ));
+      if (savedAsset != null) {
+        // immediately track assetID to avoid duplicate upload
+        await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
         final ignoreVideoFile = IgnoredFile(
           savedAsset.id,
-          savedAsset.title ?? videoTitle,
+          savedAsset.title ?? "",
           savedAsset.relativePath ?? 'remoteDownload',
           "remoteDownload",
         );
         debugPrint("IgnoreFile for auto-upload ${ignoreVideoFile.toString()}");
         await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
+        file.localID = savedAsset.id;
+        await FilesDB.instance.insert(file);
+        Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
+      } else if (!downloadLivePhotoOnDroid && savedAsset == null) {
+        _logger.severe('Failed to save assert of type $type');
       }
-    }
-
-    Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
-    await dialog.hide();
-    if (file.fileType == FileType.livePhoto) {
-      showToast(context, "Photo and video saved to gallery");
-    } else {
       showToast(context, "File saved to gallery");
+      await dialog.hide();
+    } catch (e) {
+      _logger.warning("Failed to save file", e);
+      await dialog.hide();
+      showGenericErrorDialog(context);
     }
   }
 
+  Future<void> _saveLivePhotoOnDroid(
+    io.File image,
+    io.File video,
+    File enteFile,
+  ) async {
+    debugPrint("Downloading LivePhoto on Droid");
+    AssetEntity savedAsset = await PhotoManager.editor
+        .saveImageWithPath(image.path, title: enteFile.title);
+    IgnoredFile ignoreVideoFile = IgnoredFile(
+      savedAsset.id,
+      savedAsset.title ?? '',
+      savedAsset.relativePath ?? 'remoteDownload',
+      "remoteDownload",
+    );
+    await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
+    final videoTitle = file_path.basenameWithoutExtension(enteFile.title) +
+        file_path.extension(video.path);
+    savedAsset = (await PhotoManager.editor.saveVideo(
+      video,
+      title: videoTitle,
+    ));
+    ignoreVideoFile = IgnoredFile(
+      savedAsset.id,
+      savedAsset.title ?? videoTitle,
+      savedAsset.relativePath ?? 'remoteDownload',
+      "remoteDownload",
+    );
+    await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
+  }
+
   Future<void> _setAs(File file) async {
     final dialog = createProgressDialog(context, "Please wait...");
     await dialog.show();
     try {
       final io.File fileToSave = await getFile(file);
-      var m = MediaExtension();
+      final m = MediaExtension();
       final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
       if (result == false) {
         showShortToast(context, "Something went wrong");

+ 59 - 14
lib/ui/viewer/file/fading_bottom_bar.dart

@@ -4,6 +4,7 @@ import 'dart:io';
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/file.dart';
@@ -11,6 +12,9 @@ import 'package:photos/models/file_type.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_file.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/viewer/file/file_info_widget.dart';
 import 'package:photos/utils/delete_file_util.dart';
@@ -72,8 +76,13 @@ class FadingBottomBarState extends State<FadingBottomBar> {
               Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
               color: Colors.white,
             ),
-            onPressed: () {
-              _displayInfo(widget.file);
+            onPressed: () async {
+              await _displayInfo(widget.file);
+              safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
+              await Future.delayed(
+                const Duration(milliseconds: 500),
+              ); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
+              safeRefresh();
             },
           ),
         ),
@@ -82,6 +91,15 @@ class FadingBottomBarState extends State<FadingBottomBar> {
     if (widget.file is TrashFile) {
       _addTrashOptions(children);
     }
+    final bool isUploadedByUser = widget.file.uploadedFileID != null &&
+        widget.file.ownerID == Configuration.instance.getUserID();
+    bool isFileHidden = false;
+    if (isUploadedByUser) {
+      isFileHidden = CollectionsService.instance
+              .getCollectionByID(widget.file.collectionID)
+              ?.isHidden() ??
+          false;
+    }
     if (!widget.showOnlyInfoButton && widget.file is! TrashFile) {
       if (widget.file.fileType == FileType.image ||
           widget.file.fileType == FileType.livePhoto) {
@@ -103,20 +121,17 @@ class FadingBottomBarState extends State<FadingBottomBar> {
           ),
         );
       }
-      if (widget.file.uploadedFileID != null &&
-          widget.file.ownerID == Configuration.instance.getUserID()) {
+      if (isUploadedByUser && !isFileHidden) {
         final bool isArchived =
             widget.file.magicMetadata.visibility == visibilityArchive;
         children.add(
           Tooltip(
-            message: isArchived ? "Unhide" : "Hide",
+            message: isArchived ? "Unarchive" : "Archive",
             child: Padding(
               padding: const EdgeInsets.only(top: 12, bottom: 12),
               child: IconButton(
                 icon: Icon(
-                  isArchived
-                      ? Icons.visibility_outlined
-                      : Icons.visibility_off_outlined,
+                  isArchived ? Icons.unarchive : Icons.archive_outlined,
                   color: Colors.white,
                 ),
                 onPressed: () async {
@@ -176,9 +191,31 @@ class FadingBottomBarState extends State<FadingBottomBar> {
             ),
             child: Padding(
               padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.spaceAround,
-                children: children,
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  widget.file.caption?.isNotEmpty ?? false
+                      ? Padding(
+                          padding: const EdgeInsets.fromLTRB(
+                            16,
+                            28,
+                            16,
+                            12,
+                          ),
+                          child: Text(
+                            widget.file.caption,
+                            style: getEnteTextTheme(context)
+                                .small
+                                .copyWith(color: textBaseDark),
+                            textAlign: TextAlign.center,
+                          ),
+                        )
+                      : const SizedBox.shrink(),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceAround,
+                    children: children,
+                  ),
+                ],
               ),
             ),
           ),
@@ -242,11 +279,19 @@ class FadingBottomBarState extends State<FadingBottomBar> {
   }
 
   Future<void> _displayInfo(File file) async {
-    return showModalBottomSheet<void>(
+    final colorScheme = getEnteColorScheme(context);
+    return showBarModalBottomSheet(
+      topControl: const SizedBox.shrink(),
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
+      backgroundColor: colorScheme.backgroundBase,
+      barrierColor: backdropFaintDark,
       context: context,
-      isScrollControlled: true,
       builder: (BuildContext context) {
-        return FileInfoWidget(file);
+        return Padding(
+          padding:
+              EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+          child: FileInfoWidget(file),
+        );
       },
     );
   }

+ 107 - 0
lib/ui/viewer/file/file_caption_widget.dart

@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/utils/magic_util.dart';
+
+class FileCaptionWidget extends StatefulWidget {
+  final File file;
+  const FileCaptionWidget({required this.file, super.key});
+
+  @override
+  State<FileCaptionWidget> createState() => _FileCaptionWidgetState();
+}
+
+class _FileCaptionWidgetState extends State<FileCaptionWidget> {
+  int maxLength = 280;
+  int currentLength = 0;
+  final _textController = TextEditingController();
+  final _focusNode = FocusNode();
+  String? editedCaption;
+  String? hintText = "Add a description...";
+
+  @override
+  void initState() {
+    _focusNode.addListener(() {
+      final caption = widget.file.caption;
+      if (_focusNode.hasFocus && caption != null) {
+        _textController.text = caption;
+        editedCaption = caption;
+      }
+    });
+    editedCaption = widget.file.caption;
+    if (editedCaption != null && editedCaption!.isNotEmpty) {
+      hintText = editedCaption;
+    }
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    if (editedCaption != null) {
+      editFileCaption(null, widget.file, editedCaption);
+    }
+    _textController.dispose();
+    _focusNode.removeListener(() {});
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    final textTheme = getEnteTextTheme(context);
+    return TextField(
+      onEditingComplete: () async {
+        if (editedCaption != null) {
+          await editFileCaption(context, widget.file, editedCaption);
+          if (mounted) {
+            setState(() {});
+          }
+        }
+        _focusNode.unfocus();
+      },
+      controller: _textController,
+      focusNode: _focusNode,
+      decoration: InputDecoration(
+        counterStyle: textTheme.mini.copyWith(color: colorScheme.textMuted),
+        counterText: currentLength > 99
+            ? currentLength.toString() + " / " + maxLength.toString()
+            : "",
+        contentPadding: const EdgeInsets.all(16),
+        border: OutlineInputBorder(
+          borderRadius: BorderRadius.circular(2),
+          borderSide: const BorderSide(
+            width: 0,
+            style: BorderStyle.none,
+          ),
+        ),
+        focusedBorder: OutlineInputBorder(
+          borderRadius: BorderRadius.circular(2),
+          borderSide: const BorderSide(
+            width: 0,
+            style: BorderStyle.none,
+          ),
+        ),
+        filled: true,
+        fillColor: colorScheme.fillFaint,
+        hintText: hintText,
+        hintStyle: getEnteTextTheme(context)
+            .small
+            .copyWith(color: colorScheme.textMuted),
+      ),
+      style: getEnteTextTheme(context).small,
+      cursorWidth: 1.5,
+      maxLength: maxLength,
+      minLines: 1,
+      maxLines: 6,
+      textCapitalization: TextCapitalization.sentences,
+      keyboardType: TextInputType.text,
+      onChanged: (value) {
+        setState(() {
+          hintText = "Add a description...";
+          currentLength = value.length;
+          editedCaption = value;
+        });
+      },
+    );
+  }
+}

+ 68 - 58
lib/ui/viewer/file/file_info_widget.dart

@@ -9,10 +9,13 @@ import 'package:photos/db/files_db.dart';
 import "package:photos/ente_theme_data.dart";
 import "package:photos/models/file.dart";
 import "package:photos/models/file_type.dart";
-import 'package:photos/ui/common/DividerWithPadding.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/title_bar_widget.dart';
 import 'package:photos/ui/viewer/file/collections_list_of_file_widget.dart';
 import 'package:photos/ui/viewer/file/device_folders_list_of_file_widget.dart';
-import 'package:photos/ui/viewer/file/raw_exif_button.dart';
+import 'package:photos/ui/viewer/file/file_caption_widget.dart';
+import 'package:photos/ui/viewer/file/raw_exif_list_tile_widget.dart';
 import "package:photos/utils/date_time_util.dart";
 import "package:photos/utils/exif_util.dart";
 import "package:photos/utils/file_util.dart";
@@ -51,9 +54,11 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
         widget.file.fileType == FileType.livePhoto;
     if (_isImage) {
       getExif(widget.file).then((exif) {
-        setState(() {
-          _exif = exif;
-        });
+        if (mounted) {
+          setState(() {
+            _exif = exif;
+          });
+        }
       });
     }
     super.initState();
@@ -88,9 +93,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
     final bool showDimension =
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
     final listTiles = <Widget>[
+      widget.file.uploadedFileID == null ||
+              Configuration.instance.getUserID() != file.ownerID
+          ? const SizedBox.shrink()
+          : Padding(
+              padding: const EdgeInsets.only(top: 8, bottom: 4),
+              child: FileCaptionWidget(file: widget.file),
+            ),
       ListTile(
+        horizontalTitleGap: 2,
         leading: const Padding(
-          padding: EdgeInsets.only(top: 8, left: 6),
+          padding: EdgeInsets.only(top: 8),
           child: Icon(Icons.calendar_today_rounded),
         ),
         title: Text(
@@ -119,17 +132,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               )
             : const SizedBox.shrink(),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       ListTile(
+        horizontalTitleGap: 2,
         leading: _isImage
             ? const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                   Icons.image,
                 ),
               )
             : const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                   Icons.video_camera_back,
                   size: 27,
@@ -167,13 +180,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                 icon: const Icon(Icons.edit),
               ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       showExifListTile
           ? ListTile(
-              leading: const Padding(
-                padding: EdgeInsets.only(left: 6),
-                child: Icon(Icons.camera_rounded),
-              ),
+              horizontalTitleGap: 2,
+              leading: const Icon(Icons.camera_rounded),
               title: Text(_exifData["takenOnDevice"] ?? "--"),
               subtitle: Row(
                 children: [
@@ -205,27 +215,22 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                 ],
               ),
             )
-          : const SizedBox.shrink(),
-      showExifListTile
-          ? const DividerWithPadding(left: 70, right: 20)
-          : const SizedBox.shrink(),
+          : null,
       SizedBox(
         height: 62,
         child: ListTile(
-          leading: const Padding(
-            padding: EdgeInsets.only(left: 6),
-            child: Icon(Icons.folder_outlined),
-          ),
+          horizontalTitleGap: 0,
+          leading: const Icon(Icons.folder_outlined),
           title: fileIsBackedup
               ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
         ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       (file.uploadedFileID != null && file.updationTime != null)
           ? ListTile(
+              horizontalTitleGap: 2,
               leading: const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(Icons.cloud_upload_outlined),
               ),
               title: Text(
@@ -245,48 +250,53 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                     ),
               ),
             )
-          : const SizedBox.shrink(),
-      _isImage
-          ? Padding(
-              padding: const EdgeInsets.fromLTRB(0, 24, 0, 16),
-              child: SafeArea(
-                child: RawExifButton(_exif, widget.file),
-              ),
-            )
-          : const SizedBox(
-              height: 12,
-            )
+          : null,
+      _isImage ? RawExifListTileWidget(_exif, widget.file) : null,
     ];
 
-    return Column(
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        Padding(
-          padding: const EdgeInsets.all(10),
-          child: Row(
-            crossAxisAlignment: CrossAxisAlignment.center,
-            children: [
-              IconButton(
-                onPressed: () {
-                  Navigator.pop(context);
-                },
-                icon: const Icon(
-                  Icons.close,
+    listTiles.removeWhere(
+      (element) => element == null,
+    );
+
+    return SafeArea(
+      top: false,
+      child: Scrollbar(
+        thickness: 4,
+        radius: const Radius.circular(2),
+        thumbVisibility: true,
+        child: Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: CustomScrollView(
+            shrinkWrap: true,
+            slivers: <Widget>[
+              TitleBarWidget(
+                isFlexibleSpaceDisabled: true,
+                title: "Details",
+                isOnTopOfScreen: false,
+                leading: IconButtonWidget(
+                  icon: Icons.close_outlined,
+                  iconButtonType: IconButtonType.primary,
+                  onTap: () => Navigator.pop(context),
                 ),
               ),
-              const SizedBox(width: 6),
-              Padding(
-                padding: const EdgeInsets.only(bottom: 2),
-                child: Text(
-                  "Details",
-                  style: Theme.of(context).textTheme.bodyText1,
+              SliverList(
+                delegate: SliverChildBuilderDelegate(
+                  (context, index) {
+                    if (index.isOdd) {
+                      return index == 1
+                          ? const SizedBox.shrink()
+                          : const DividerWidget(dividerType: DividerType.menu);
+                    } else {
+                      return listTiles[index ~/ 2];
+                    }
+                  },
+                  childCount: (listTiles.length * 2) - 1,
                 ),
-              ),
+              )
             ],
           ),
         ),
-        ...listTiles
-      ],
+      ),
     );
   }
 

+ 0 - 100
lib/ui/viewer/file/raw_exif_button.dart

@@ -1,100 +0,0 @@
-// @dart=2.9
-
-import 'package:exif/exif.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:photos/ente_theme_data.dart';
-import "package:photos/models/file.dart";
-import 'package:photos/ui/viewer/file/exif_info_dialog.dart';
-import 'package:photos/utils/toast_util.dart';
-
-enum Status {
-  loading,
-  exifIsAvailable,
-  noExif,
-}
-
-class RawExifButton extends StatelessWidget {
-  final File file;
-  final Map<String, IfdTag> exif;
-  const RawExifButton(this.exif, this.file, {Key key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    Status exifStatus = Status.loading;
-    if (exif == null) {
-      exifStatus = Status.loading;
-    } else if (exif.isNotEmpty) {
-      exifStatus = Status.exifIsAvailable;
-    } else {
-      exifStatus = Status.noExif;
-    }
-    return GestureDetector(
-      onTap:
-          exifStatus == Status.loading || exifStatus == Status.exifIsAvailable
-              ? () {
-                  showDialog(
-                    context: context,
-                    builder: (BuildContext context) {
-                      return ExifInfoDialog(file);
-                    },
-                    barrierColor: Colors.black87,
-                  );
-                }
-              : exifStatus == Status.noExif
-                  ? () {
-                      showShortToast(context, "This image has no exif data");
-                    }
-                  : null,
-      child: Container(
-        height: 40,
-        width: 140,
-        decoration: BoxDecoration(
-          color: Theme.of(context)
-              .colorScheme
-              .inverseBackgroundColor
-              .withOpacity(0.12),
-          borderRadius: const BorderRadius.all(
-            Radius.circular(20),
-          ),
-        ),
-        child: Center(
-          child: exifStatus == Status.loading
-              ? Row(
-                  mainAxisAlignment: MainAxisAlignment.center,
-                  children: const [
-                    CupertinoActivityIndicator(
-                      radius: 8,
-                    ),
-                    SizedBox(
-                      width: 8,
-                    ),
-                    Text('EXIF')
-                  ],
-                )
-              : exifStatus == Status.exifIsAvailable
-                  ? Row(
-                      mainAxisAlignment: MainAxisAlignment.center,
-                      children: const [
-                        Icon(Icons.feed_outlined),
-                        SizedBox(
-                          width: 8,
-                        ),
-                        Text('Raw EXIF'),
-                      ],
-                    )
-                  : Row(
-                      mainAxisAlignment: MainAxisAlignment.center,
-                      children: const [
-                        Icon(Icons.feed_outlined),
-                        SizedBox(
-                          width: 8,
-                        ),
-                        Text('No EXIF'),
-                      ],
-                    ),
-        ),
-      ),
-    );
-  }
-}

+ 71 - 0
lib/ui/viewer/file/raw_exif_list_tile_widget.dart

@@ -0,0 +1,71 @@
+// @dart=2.9
+
+import 'package:exif/exif.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/ente_theme_data.dart';
+import "package:photos/models/file.dart";
+import 'package:photos/ui/viewer/file/exif_info_dialog.dart';
+import 'package:photos/utils/toast_util.dart';
+
+enum Status {
+  loading,
+  exifIsAvailable,
+  noExif,
+}
+
+class RawExifListTileWidget extends StatelessWidget {
+  final File file;
+  final Map<String, IfdTag> exif;
+  const RawExifListTileWidget(this.exif, this.file, {Key key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    Status exifStatus = Status.loading;
+    if (exif == null) {
+      exifStatus = Status.loading;
+    } else if (exif.isNotEmpty) {
+      exifStatus = Status.exifIsAvailable;
+    } else {
+      exifStatus = Status.noExif;
+    }
+    return GestureDetector(
+      onTap: exifStatus == Status.exifIsAvailable
+          ? () {
+              showDialog(
+                context: context,
+                builder: (BuildContext context) {
+                  return ExifInfoDialog(file);
+                },
+                barrierColor: Colors.black87,
+              );
+            }
+          : exifStatus == Status.noExif
+              ? () {
+                  showShortToast(context, "This image has no exif data");
+                }
+              : null,
+      child: ListTile(
+        horizontalTitleGap: 2,
+        leading: const Padding(
+          padding: EdgeInsets.only(top: 8),
+          child: Icon(Icons.feed_outlined),
+        ),
+        title: const Text("EXIF"),
+        subtitle: Text(
+          exifStatus == Status.loading
+              ? "Loading EXIF data.."
+              : exifStatus == Status.exifIsAvailable
+                  ? "View all EXIF data"
+                  : "No EXIF data",
+          style: Theme.of(context).textTheme.bodyText2.copyWith(
+                color: Theme.of(context)
+                    .colorScheme
+                    .defaultTextColor
+                    .withOpacity(0.5),
+              ),
+        ),
+      ),
+    );
+  }
+}

+ 3 - 1
lib/ui/viewer/file/video_widget.dart

@@ -78,7 +78,9 @@ class _VideoWidgetState extends State<VideoWidget> {
           .getFileSize(widget.file.uploadedFileID)
           .then((value) {
         widget.file.fileSize = value;
-        setState(() {});
+        if (mounted) {
+          setState(() {});
+        }
       });
     }
   }

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

@@ -66,7 +66,7 @@ class ArchivePage extends StatelessWidget {
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
           appBarType,
-          "Hidden",
+          "Archive",
           _selectedFiles,
         ),
       ),

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

@@ -10,6 +10,7 @@ import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/ignored_files_service.dart';
+import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
@@ -19,16 +20,21 @@ class CollectionPage extends StatelessWidget {
   final String tagPrefix;
   final GalleryType appBarType;
   final _selectedFiles = SelectedFiles();
+  bool hasVerifiedLock;
 
   CollectionPage(
     this.c, {
     this.tagPrefix = "collection",
     this.appBarType = GalleryType.ownedCollection,
+    this.hasVerifiedLock = false,
     Key key,
   }) : super(key: key);
 
   @override
   Widget build(Object context) {
+    if (hasVerifiedLock == false && c.collection.isHidden()) {
+      return const EmptyState();
+    }
     final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
@@ -55,6 +61,7 @@ class CollectionPage extends StatelessWidget {
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
+        EventType.hide,
       },
       tagPrefix: tagPrefix,
       selectedFiles: _selectedFiles,

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä