فهرست منبع

Merge branch 'master' into empty_albums

Neeraj Gupta 2 سال پیش
والد
کامیت
bbbc61fd4e
100فایلهای تغییر یافته به همراه3162 افزوده شده و 1562 حذف شده
  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_constructors: warning
     prefer_const_declarations: warning
     prefer_const_declarations: warning
     prefer_const_constructors_in_immutables: warning
     prefer_const_constructors_in_immutables: warning
+    prefer_final_locals: warning
     unnecessary_const: error
     unnecessary_const: error
     cancel_subscriptions: error
     cancel_subscriptions: error
 
 
+    invalid_dependency: info
     use_build_context_synchronously: ignore # experimental lint, requires many changes
     use_build_context_synchronously: ignore # experimental lint, requires many changes
     prefer_interpolation_to_compose_strings: ignore # later too many warnings
     prefer_interpolation_to_compose_strings: ignore # later too many warnings
     prefer_double_quotes: ignore # too many warnings
     prefer_double_quotes: ignore # too many warnings

+ 2 - 2
android/app/build.gradle

@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
 }
 }
 
 
 android {
 android {
-    compileSdkVersion 32
+    compileSdkVersion 33
 
 
     sourceSets {
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
         main.java.srcDirs += 'src/main/kotlin'
@@ -47,7 +47,7 @@ android {
     defaultConfig {
     defaultConfig {
         applicationId "io.ente.photos"
         applicationId "io.ente.photos"
         minSdkVersion 19
         minSdkVersion 19
-        targetSdkVersion 30
+        targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
         versionName flutterVersionName
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         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>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
             </intent-filter>
 
 
             <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"/>
                 <data android:scheme="ente"/>
             </intent-filter>
             </intent-filter>
 
 
             <!--Filter to support sharing images into our app-->
             <!--Filter to support sharing images into our app-->
             <intent-filter android:label="@string/backup">
             <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>
 
 
             <intent-filter android:label="@string/backup">
             <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>
 
 
             <intent-filter android:label="@string/backup">
             <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>
 
 
             <intent-filter android:label="@string/backup">
             <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>
             </intent-filter>
 
 
         </activity>
         </activity>
         <!-- Don't delete the meta-data below.
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
              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>
     </application>
 
 
     <!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
     <!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
     <!-- https://developer.android.com/training/package-visibility/use-cases -->
     <!-- https://developer.android.com/training/package-visibility/use-cases -->
     <queries>
     <queries>
         <intent>
         <intent>
-            <action android:name="android.intent.action.SENDTO" />
-            <data android:scheme="mailto" />
+            <action android:name="android.intent.action.SENDTO"/>
+            <data android:scheme="mailto"/>
         </intent>
         </intent>
     </queries>
     </queries>
     <uses-permission android:name="android.permission.INTERNET"/>
     <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>
 </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
   FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
   FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
   FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
   flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
   flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
   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 String roadmapURL = "https://roadmap.ente.io";
 const int microSecondsInDay = 86400000000;
 const int microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
 const int android11SDKINT = 30;
+const int jan011991Time = 31580904000000;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 
 

+ 4 - 2
lib/core/network.dart

@@ -66,8 +66,10 @@ class EnteRequestInterceptor extends Interceptor {
   @override
   @override
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
     if (kDebugMode) {
     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
     // ignore: prefer_const_constructors
     options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
     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);
         deviceCollections.add(deviceCollection);
       }
       }
       if (includeCoverThumbnail) {
       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;
       return deviceCollections;
     } catch (e) {
     } 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/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.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:photos/utils/file_uploader_util.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite_migration/sqflite_migration.dart';
 import 'package:sqflite_migration/sqflite_migration.dart';
@@ -611,17 +610,9 @@ class FilesDB {
   }) async {
   }) async {
     final db = await instance.database;
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
     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(
     final results = await db.query(
       filesTable,
       filesTable,
@@ -636,6 +627,43 @@ class FilesDB {
     return FileLoadResult(files, files.length == limit);
     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(
   Future<List<File>> getFilesCreatedWithinDurations(
     List<List<int>> durations,
     List<List<int>> durations,
     Set<int> ignoredCollectionIDs, {
     Set<int> ignoredCollectionIDs, {
@@ -1080,7 +1108,9 @@ class FilesDB {
     final db = await instance.database;
     final db = await instance.database;
     final count = Sqflite.firstIntValue(
     final count = Sqflite.firstIntValue(
       await db.rawQuery(
       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;
     return count;
@@ -1143,25 +1173,7 @@ class FilesDB {
 
 
   Future<List<File>> getLatestCollectionFiles() async {
   Future<List<File>> getLatestCollectionFiles() async {
     debugPrint("Fetching latestCollectionFiles from db");
     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.*
       SELECT $filesTable.*
       FROM $filesTable
       FROM $filesTable
       INNER JOIN
       INNER JOIN
@@ -1173,9 +1185,7 @@ class FilesDB {
         ) latest_files
         ) latest_files
         ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
         ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
         AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
         AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
-
   ''';
   ''';
-    }
     final db = await instance.database;
     final db = await instance.database;
     final rows = await db.rawQuery(
     final rows = await db.rawQuery(
       query,
       query,
@@ -1250,6 +1260,33 @@ class FilesDB {
     return result;
     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(
   Future<Set<int>> getAllCollectionIDsOfFile(
     int uploadedFileID,
     int uploadedFileID,
   ) async {
   ) async {
@@ -1276,15 +1313,28 @@ class FilesDB {
     return files;
     return files;
   }
   }
 
 
-  Future<List<File>> getAllFilesFromDB() async {
+  Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
     final db = await instance.database;
     final db = await instance.database;
     final List<Map<String, dynamic>> result = await db.query(filesTable);
     final List<Map<String, dynamic>> result = await db.query(filesTable);
     final List<File> files = convertToFiles(result);
     final List<File> files = convertToFiles(result);
     final List<File> deduplicatedFiles =
     final List<File> deduplicatedFiles =
-        _deduplicatedAndFilterIgnoredFiles(files, null);
+        _deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
     return deduplicatedFiles;
     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) {
   Map<String, dynamic> _getRowForFile(File file) {
     final row = <String, dynamic>{};
     final row = <String, dynamic>{};
     if (file.generatedID != null) {
     if (file.generatedID != null) {

+ 2 - 0
lib/events/files_updated_event.dart

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

+ 6 - 0
lib/events/tab_changed_event.dart

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

+ 1 - 0
lib/models/gallery_type.dart

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

+ 25 - 2
lib/models/magic_metadata.dart

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

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

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

+ 17 - 0
lib/models/user_details.dart

@@ -2,6 +2,7 @@ import 'dart:math';
 
 
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:equatable/equatable.dart';
 import 'package:equatable/equatable.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/subscription.dart';
 import 'package:photos/models/subscription.dart';
 
 
 class UserDetails extends Equatable {
 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/files_updated_event.dart';
 import 'package:photos/events/force_reload_home_gallery_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/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.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
@@ -51,6 +52,7 @@ class CollectionsService {
   final _localPathToCollectionID = <String, int>{};
   final _localPathToCollectionID = <String, int>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _cachedKeys = <int, Uint8List>{};
   final _cachedKeys = <int, Uint8List>{};
+  Collection cachedDefaultHiddenCollection;
 
 
   CollectionsService._privateConstructor() {
   CollectionsService._privateConstructor() {
     _db = CollectionsDB.instance;
     _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 {
   Future<List<Collection>> sync() async {
     _logger.info("Syncing collections");
     _logger.info("Syncing collections");
     final lastCollectionUpdationTime =
     final lastCollectionUpdationTime =
@@ -145,6 +156,22 @@ class CollectionsService {
         .toSet();
         .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) {
   int getCollectionSyncTime(int collectionID) {
     return _prefs
     return _prefs
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
@@ -177,6 +204,8 @@ class CollectionsService {
   }) async {
   }) async {
     final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
     final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
     final usersCollection = getActiveCollections();
     final usersCollection = getActiveCollections();
+    // remove any hidden collection to avoid accidental rendering on UI
+    usersCollection.removeWhere((element) => element.isHidden());
     if (!includedOwnedByOthers) {
     if (!includedOwnedByOthers) {
       final userID = Configuration.instance.getUserID();
       final userID = Configuration.instance.getUserID();
       usersCollection.removeWhere((c) => c.owner.id != userID);
       usersCollection.removeWhere((c) => c.owner.id != userID);
@@ -298,6 +327,7 @@ class CollectionsService {
   }
   }
 
 
   Uint8List _getDecryptedKey(Collection collection) {
   Uint8List _getDecryptedKey(Collection collection) {
+    debugPrint("Finding collection decryption key for ${collection.id}");
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
     if (collection.owner.id == _config.getUserID()) {
     if (collection.owner.id == _config.getUserID()) {
       if (_config.getKey() == null) {
       if (_config.getKey() == null) {
@@ -820,17 +850,17 @@ class CollectionsService {
     List<File> files,
     List<File> files,
   ) {
   ) {
     if (toCollectionID == fromCollectionID) {
     if (toCollectionID == fromCollectionID) {
-      throw AssertionError("can't move to same album");
+      throw AssertionError("Can't move to same album");
     }
     }
     for (final file in files) {
     for (final file in files) {
       if (file.uploadedFileID == null) {
       if (file.uploadedFileID == null) {
-        throw AssertionError("can only move uploaded memories");
+        throw AssertionError("Can only move uploaded memories");
       }
       }
       if (file.collectionID != fromCollectionID) {
       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()) {
       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);
     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
     return _enteDio
         .post(
         .post(
       "/collections",
       "/collections",
-      data: collection.toMap(),
+      data: payload,
     )
     )
         .then((response) {
         .then((response) {
       final collection = Collection.fromMap(response.data["collection"]);
       final collection = Collection.fromMap(response.data["collection"]);

+ 2 - 4
lib/services/feature_flag_service.dart

@@ -73,10 +73,8 @@ class FeatureFlagService {
           .getDio()
           .getDio()
           .get("https://static.ente.io/feature_flags.json");
           .get("https://static.ente.io/feature_flags.json");
       final flagsResponse = FeatureFlags.fromMap(response.data);
       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) {
     } catch (e) {
       _logger.severe("Failed to sync feature flags ", 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,
       size: assetFetchPageSize,
     );
     );
     Bus.instance.fire(
     Bus.instance.fire(
-      LocalImportProgressEvent(pathEntity.name,
-          currentPage * assetFetchPageSize + currentPageResult.length),
+      LocalImportProgressEvent(
+        pathEntity.name,
+        currentPage * assetFetchPageSize + currentPageResult.length,
+      ),
     );
     );
     result.addAll(currentPageResult);
     result.addAll(currentPageResult);
     currentPage = currentPage + 1;
     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;
           date.add(const Duration(days: daysAfter)).microsecondsSinceEpoch;
       durations.add([startCreationTime, endCreationTime]);
       durations.add([startCreationTime, endCreationTime]);
     }
     }
-    final archivedCollectionIds =
-        CollectionsService.instance.getArchivedCollections();
+    final ignoredCollections =
+        CollectionsService.instance.collectionsHiddenFromTimeline();
     final files = await _filesDB.getFilesCreatedWithinDurations(
     final files = await _filesDB.getFilesCreatedWithinDurations(
       durations,
       durations,
-      archivedCollectionIds,
+      ignoredCollections,
     );
     );
     final seenTimes = await _memoriesDB.getSeenTimes();
     final seenTimes = await _memoriesDB.getSeenTimes();
     final List<Memory> memories = [];
     final List<Memory> memories = [];

+ 10 - 6
lib/services/remote_sync_service.dart

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

+ 39 - 16
lib/services/search_service.dart

@@ -32,28 +32,23 @@ class SearchService {
   static final SearchService instance = SearchService._privateConstructor();
   static final SearchService instance = SearchService._privateConstructor();
 
 
   Future<void> init() async {
   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) {
     Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
       // only invalidate, let the load happen on demand
       // only invalidate, let the load happen on demand
       _cachedFilesFuture = null;
       _cachedFilesFuture = null;
     });
     });
   }
   }
 
 
+  Set<int> ignoreCollections() {
+    return CollectionsService.instance.getHiddenCollections();
+  }
+
   Future<List<File>> _getAllFiles() async {
   Future<List<File>> _getAllFiles() async {
     if (_cachedFilesFuture != null) {
     if (_cachedFilesFuture != null) {
       return _cachedFilesFuture;
       return _cachedFilesFuture;
     }
     }
     _logger.fine("Reading all files from db");
     _logger.fine("Reading all files from db");
-    _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
+    _cachedFilesFuture =
+        FilesDB.instance.getAllFilesFromDB(ignoreCollections());
     return _cachedFilesFuture;
     return _cachedFilesFuture;
   }
   }
 
 
@@ -133,7 +128,11 @@ class SearchService {
       if (collectionSearchResults.length >= _maximumResultsLimit) {
       if (collectionSearchResults.length >= _maximumResultsLimit) {
         break;
         break;
       }
       }
-      if (c.collection.name.toLowerCase().contains(query.toLowerCase())) {
+
+      if (!c.collection.isHidden() &&
+          c.collection.name.toLowerCase().contains(
+                query.toLowerCase(),
+              )) {
         collectionSearchResults.add(AlbumSearchResult(c));
         collectionSearchResults.add(AlbumSearchResult(c));
       }
       }
     }
     }
@@ -172,7 +171,7 @@ class SearchService {
         final matchedFiles =
         final matchedFiles =
             await FilesDB.instance.getFilesCreatedWithinDurations(
             await FilesDB.instance.getFilesCreatedWithinDurations(
           _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
           _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
-          null,
+          ignoreCollections(),
           order: 'DESC',
           order: 'DESC',
         );
         );
         if (matchedFiles.isNotEmpty) {
         if (matchedFiles.isNotEmpty) {
@@ -209,6 +208,30 @@ class SearchService {
     return searchResults;
     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(
   Future<List<GenericSearchResult>> getFileExtensionResults(
     String query,
     String query,
   ) async {
   ) async {
@@ -248,7 +271,7 @@ class SearchService {
       final matchedFiles =
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsOfMonthInEveryYear(month.monthNumber),
         _getDurationsOfMonthInEveryYear(month.monthNumber),
-        null,
+        ignoreCollections(),
         order: 'DESC',
         order: 'DESC',
       );
       );
       if (matchedFiles.isNotEmpty) {
       if (matchedFiles.isNotEmpty) {
@@ -277,7 +300,7 @@ class SearchService {
       final matchedFiles =
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsForCalendarDateInEveryYear(day, month, year: year),
         _getDurationsForCalendarDateInEveryYear(day, month, year: year),
-        null,
+        ignoreCollections(),
         order: 'DESC',
         order: 'DESC',
       );
       );
       if (matchedFiles.isNotEmpty) {
       if (matchedFiles.isNotEmpty) {
@@ -305,7 +328,7 @@ class SearchService {
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
     return await FilesDB.instance.getFilesCreatedWithinDurations(
     return await FilesDB.instance.getFilesCreatedWithinDurations(
       [durationOfYear],
       [durationOfYear],
-      null,
+      ignoreCollections(),
       order: "DESC",
       order: "DESC",
     );
     );
   }
   }

+ 34 - 4
lib/theme/colors.dart

@@ -11,11 +11,13 @@ class EnteColorScheme {
   // Backdrop Colors
   // Backdrop Colors
   final Color backdropBase;
   final Color backdropBase;
   final Color backdropBaseMute;
   final Color backdropBaseMute;
+  final Color backdropFaint;
 
 
   // Text Colors
   // Text Colors
   final Color textBase;
   final Color textBase;
   final Color textMuted;
   final Color textMuted;
   final Color textFaint;
   final Color textFaint;
+  final Color blurTextBase;
 
 
   // Fill Colors
   // Fill Colors
   final Color fillBase;
   final Color fillBase;
@@ -27,6 +29,9 @@ class EnteColorScheme {
   final Color strokeMuted;
   final Color strokeMuted;
   final Color strokeFaint;
   final Color strokeFaint;
   final Color strokeFainter;
   final Color strokeFainter;
+  final Color blurStrokeBase;
+  final Color blurStrokeFaint;
+  final Color blurStrokePressed;
 
 
   // Fixed Colors
   // Fixed Colors
   final Color primary700;
   final Color primary700;
@@ -49,9 +54,11 @@ class EnteColorScheme {
     this.backgroundElevated2,
     this.backgroundElevated2,
     this.backdropBase,
     this.backdropBase,
     this.backdropBaseMute,
     this.backdropBaseMute,
+    this.backdropFaint,
     this.textBase,
     this.textBase,
     this.textMuted,
     this.textMuted,
     this.textFaint,
     this.textFaint,
+    this.blurTextBase,
     this.fillBase,
     this.fillBase,
     this.fillMuted,
     this.fillMuted,
     this.fillFaint,
     this.fillFaint,
@@ -59,6 +66,9 @@ class EnteColorScheme {
     this.strokeMuted,
     this.strokeMuted,
     this.strokeFaint,
     this.strokeFaint,
     this.strokeFainter,
     this.strokeFainter,
+    this.blurStrokeBase,
+    this.blurStrokeFaint,
+    this.blurStrokePressed,
     this.tabIcon, {
     this.tabIcon, {
     this.primary700 = _primary700,
     this.primary700 = _primary700,
     this.primary500 = _primary500,
     this.primary500 = _primary500,
@@ -76,10 +86,12 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   backgroundElevatedLight,
   backgroundElevatedLight,
   backgroundElevated2Light,
   backgroundElevated2Light,
   backdropBaseLight,
   backdropBaseLight,
-  backdropBaseMuteLight,
+  backdropMutedLight,
+  backdropFaintLight,
   textBaseLight,
   textBaseLight,
   textMutedLight,
   textMutedLight,
   textFaintLight,
   textFaintLight,
+  blurTextBaseLight,
   fillBaseLight,
   fillBaseLight,
   fillMutedLight,
   fillMutedLight,
   fillFaintLight,
   fillFaintLight,
@@ -87,6 +99,9 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   strokeMutedLight,
   strokeMutedLight,
   strokeFaintLight,
   strokeFaintLight,
   strokeFainterLight,
   strokeFainterLight,
+  blurStrokeBaseLight,
+  blurStrokeFaintLight,
+  blurStrokePressedLight,
   tabIconLight,
   tabIconLight,
 );
 );
 
 
@@ -95,10 +110,12 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   backgroundElevatedDark,
   backgroundElevatedDark,
   backgroundElevated2Dark,
   backgroundElevated2Dark,
   backdropBaseDark,
   backdropBaseDark,
-  backdropBaseMuteDark,
+  backdropMutedDark,
+  backdropFaintDark,
   textBaseDark,
   textBaseDark,
   textMutedDark,
   textMutedDark,
   textFaintDark,
   textFaintDark,
+  blurTextBaseDark,
   fillBaseDark,
   fillBaseDark,
   fillMutedDark,
   fillMutedDark,
   fillFaintDark,
   fillFaintDark,
@@ -106,6 +123,9 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   strokeMutedDark,
   strokeMutedDark,
   strokeFaintDark,
   strokeFaintDark,
   strokeFainterDark,
   strokeFainterDark,
+  blurStrokeBaseDark,
+  blurStrokeFaintDark,
+  blurStrokePressedDark,
   tabIconDark,
   tabIconDark,
 );
 );
 
 
@@ -120,19 +140,23 @@ const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
 
 
 // Backdrop Colors
 // Backdrop Colors
 const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75);
 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 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
 // Text Colors
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color textMutedLight = Color.fromRGBO(0, 0, 0, 0.6);
 const Color textMutedLight = Color.fromRGBO(0, 0, 0, 0.6);
 const Color textFaintLight = Color.fromRGBO(0, 0, 0, 0.5);
 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 textBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color textMutedDark = Color.fromRGBO(255, 255, 255, 0.7);
 const Color textMutedDark = Color.fromRGBO(255, 255, 255, 0.7);
 const Color textFaintDark = Color.fromRGBO(255, 255, 255, 0.5);
 const Color textFaintDark = Color.fromRGBO(255, 255, 255, 0.5);
+const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
 
 
 // Fill Colors
 // Fill Colors
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
 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 strokeMutedLight = Color.fromRGBO(0, 0, 0, 0.24);
 const Color strokeFaintLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color strokeFaintLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color strokeFainterLight = Color.fromRGBO(0, 0, 0, 0.06);
 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 strokeBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color strokeMutedDark = Color.fromRGBO(255, 255, 255, 0.24);
 const Color strokeMutedDark = Color.fromRGBO(255, 255, 255, 0.24);
 const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color strokeFainterDark = Color.fromRGBO(255, 255, 255, 0.08);
 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
 // Other colors
 const Color tabIconLight = Color.fromRGBO(0, 0, 0, 0.85);
 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 FontWeight _boldWeight = FontWeight.w600;
 const String _fontFamily = 'Inter';
 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(
 const TextStyle h1 = TextStyle(
   fontSize: 48,
   fontSize: 48,
   height: 48 / 28,
   height: 48 / 28,
@@ -31,7 +43,7 @@ const TextStyle large = TextStyle(
 );
 );
 const TextStyle body = TextStyle(
 const TextStyle body = TextStyle(
   fontSize: 16,
   fontSize: 16,
-  height: 19.4 / 16.0,
+  height: 20 / 16.0,
   fontWeight: _regularWeight,
   fontWeight: _regularWeight,
   fontFamily: _fontFamily,
   fontFamily: _fontFamily,
 );
 );
@@ -71,6 +83,8 @@ class EnteTextTheme {
   final TextStyle miniBold;
   final TextStyle miniBold;
   final TextStyle tiny;
   final TextStyle tiny;
   final TextStyle tinyBold;
   final TextStyle tinyBold;
+  final TextStyle brandSmall;
+  final TextStyle brandMedium;
 
 
   const EnteTextTheme({
   const EnteTextTheme({
     required this.h1,
     required this.h1,
@@ -89,6 +103,8 @@ class EnteTextTheme {
     required this.miniBold,
     required this.miniBold,
     required this.tiny,
     required this.tiny,
     required this.tinyBold,
     required this.tinyBold,
+    required this.brandSmall,
+    required this.brandMedium,
   });
   });
 }
 }
 
 
@@ -113,5 +129,7 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
     miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
     miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
     tiny: tiny.copyWith(color: color),
     tiny: tiny.copyWith(color: color),
     tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
     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;
             : 120;
 
 
     return Scaffold(
     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) {
   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(
                       SizedBox(
                         width: double.infinity,
                         width: double.infinity,
                         child: Text(
                         child: Text(
-                          'Verify recovery key',
+                          'Confirm recovery key',
                           style: enteTheme.textTheme.h3Bold,
                           style: enteTheme.textTheme.h3Bold,
                           textAlign: TextAlign.left,
                           textAlign: TextAlign.left,
                         ),
                         ),
                       ),
                       ),
                       const SizedBox(height: 18),
                       const SizedBox(height: 18),
                       Text(
                       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
                         style: enteTheme.textTheme.small
                             .copyWith(color: enteTheme.colorScheme.textMuted),
                             .copyWith(color: enteTheme.colorScheme.textMuted),
                       ),
                       ),
@@ -187,12 +185,6 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                         },
                         },
                       ),
                       ),
                       const SizedBox(height: 12),
                       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(
                       Expanded(
                         child: Container(
                         child: Container(
                           alignment: Alignment.bottomCenter,
                           alignment: Alignment.bottomCenter,
@@ -204,8 +196,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                             children: [
                             children: [
                               GradientButton(
                               GradientButton(
                                 onTap: _verifyRecoveryKey,
                                 onTap: _verifyRecoveryKey,
-                                text: "Verify",
-                                iconData: Icons.shield_outlined,
+                                text: "Confirm",
                               ),
                               ),
                               const SizedBox(height: 8),
                               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>(
               FutureBuilder<int>(
                 future: FilesDB.instance.collectionFileCount(c.collection.id),
                 future: FilesDB.instance.collectionFileCount(c.collection.id),
                 builder: (context, snapshot) {
                 builder: (context, snapshot) {
-                  if (snapshot.hasData && snapshot.data! > 0) {
+                  if (snapshot.hasData) {
                     return Text(
                     return Text(
                       snapshot.data.toString(),
                       snapshot.data.toString(),
                       style: enteTextTheme.small.copyWith(
                       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
 // @dart=2.9
 
 
 import 'package:flutter/material.dart';
 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';
 import 'package:photos/utils/navigation_util.dart';
 
 
 class HiddenCollectionsButtonWidget extends StatelessWidget {
 class HiddenCollectionsButtonWidget extends StatelessWidget {
@@ -44,44 +42,25 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
                     color: Theme.of(context).iconTheme.color,
                     color: Theme.of(context).iconTheme.color,
                   ),
                   ),
                   const Padding(padding: EdgeInsets.all(6)),
                   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 {
       onPressed: () async {
-        routeToPage(
+        final hasAuthenticated = await LocalAuthenticationService.instance
+            .requestLocalAuthentication(
           context,
           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:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/theme/text_style.dart';
 
 
 class SectionTitle extends StatelessWidget {
 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);
   }) : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   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(
     return Container(
       margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
       margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
       child: Column(
       child: Column(
         children: [
         children: [
           Align(
           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.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.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/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/hidden_collections_button_widget.dart';
 import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
 import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/section_title.dart';
@@ -124,7 +124,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
         child: Column(
         child: Column(
           children: [
           children: [
             const SizedBox(height: 12),
             const SizedBox(height: 12),
-            const SectionTitle("On device"),
+            const SectionTitle(title: "On device"),
             const SizedBox(height: 12),
             const SizedBox(height: 12),
             const DeviceFoldersGridViewWidget(),
             const DeviceFoldersGridViewWidget(),
             const Padding(padding: EdgeInsets.all(4)),
             const Padding(padding: EdgeInsets.all(4)),
@@ -133,7 +133,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               crossAxisAlignment: CrossAxisAlignment.end,
               crossAxisAlignment: CrossAxisAlignment.end,
               children: [
               children: [
-                const EnteSectionTitle(),
+                SectionTitle(titleWithBrand: getOnEnteSection(context)),
                 _sortMenu(),
                 _sortMenu(),
               ],
               ],
             ),
             ),
@@ -148,9 +148,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               padding: const EdgeInsets.symmetric(horizontal: 16),
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Column(
               child: Column(
                 children: [
                 children: [
-                  TrashButtonWidget(trashAndHiddenTextStyle),
+                  ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
                   const SizedBox(height: 12),
                   const SizedBox(height: 12),
                   HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
                   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/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
 
 
 class EnteLoadingWidget extends StatelessWidget {
 class EnteLoadingWidget extends StatelessWidget {
-  const EnteLoadingWidget({Key? key}) : super(key: key);
+  final Color? color;
+  const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Center(
     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(
     return Flexible(
       child: Padding(
       child: Padding(
-        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 2),
+        padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
         child: Row(
         child: Row(
           children: [
           children: [
             Flexible(
             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
         MediaQuery.of(context).platformBrightness == Brightness.light
             ? enteColorScheme.backgroundElevated2
             ? enteColorScheme.backgroundElevated2
             : enteColorScheme.backgroundElevated;
             : 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:flutter/material.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/opened_settings_event.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';
 import 'package:photos/ui/viewer/search/search_widget.dart';
 
 
 class HomeHeaderWidget extends StatefulWidget {
 class HomeHeaderWidget extends StatefulWidget {
@@ -17,30 +16,23 @@ class HomeHeaderWidget extends StatefulWidget {
 class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
 class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
   @override
   @override
   Widget build(BuildContext context) {
   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 {
 class MenuItemWidget extends StatefulWidget {
   final Widget captionedTextWidget;
   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 IconData? leadingIcon;
   final Color? leadingIconColor;
   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 IconData? trailingIcon;
   final Widget? trailingSwitch;
   final Widget? trailingSwitch;
   final bool trailingIconIsMuted;
   final bool trailingIconIsMuted;
@@ -17,10 +21,16 @@ class MenuItemWidget extends StatefulWidget {
   final Color? menuItemColor;
   final Color? menuItemColor;
   final bool alignCaptionedTextToLeft;
   final bool alignCaptionedTextToLeft;
   final double borderRadius;
   final double borderRadius;
+  final Color? pressedColor;
   final ExpandableController? expandableController;
   final ExpandableController? expandableController;
+  final bool isBottomBorderRadiusRemoved;
+  final bool isTopBorderRadiusRemoved;
+
+  /// disable gesture detector if not used
+  final bool isGestureDetectorDisabled;
   const MenuItemWidget({
   const MenuItemWidget({
     required this.captionedTextWidget,
     required this.captionedTextWidget,
-    this.isHeaderOfExpansion = false,
+    this.isExpandable = false,
     this.leadingIcon,
     this.leadingIcon,
     this.leadingIconColor,
     this.leadingIconColor,
     this.trailingIcon,
     this.trailingIcon,
@@ -31,7 +41,11 @@ class MenuItemWidget extends StatefulWidget {
     this.menuItemColor,
     this.menuItemColor,
     this.alignCaptionedTextToLeft = false,
     this.alignCaptionedTextToLeft = false,
     this.borderRadius = 4.0,
     this.borderRadius = 4.0,
+    this.pressedColor,
     this.expandableController,
     this.expandableController,
+    this.isBottomBorderRadiusRemoved = false,
+    this.isTopBorderRadiusRemoved = false,
+    this.isGestureDetectorDisabled = false,
     Key? key,
     Key? key,
   }) : super(key: key);
   }) : super(key: key);
 
 
@@ -40,8 +54,10 @@ class MenuItemWidget extends StatefulWidget {
 }
 }
 
 
 class _MenuItemWidgetState extends State<MenuItemWidget> {
 class _MenuItemWidgetState extends State<MenuItemWidget> {
+  Color? menuItemColor;
   @override
   @override
   void initState() {
   void initState() {
+    menuItemColor = widget.menuItemColor;
     if (widget.expandableController != null) {
     if (widget.expandableController != null) {
       widget.expandableController!.addListener(() {
       widget.expandableController!.addListener(() {
         setState(() {});
         setState(() {});
@@ -50,6 +66,12 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
     super.initState();
     super.initState();
   }
   }
 
 
+  @override
+  void didChangeDependencies() {
+    menuItemColor = widget.menuItemColor;
+    super.didChangeDependencies();
+  }
+
   @override
   @override
   void dispose() {
   void dispose() {
     if (widget.expandableController != null) {
     if (widget.expandableController != null) {
@@ -60,11 +82,14 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return widget.isHeaderOfExpansion
+    return widget.isExpandable || widget.isGestureDetectorDisabled
         ? menuItemWidget(context)
         ? menuItemWidget(context)
         : GestureDetector(
         : GestureDetector(
             onTap: widget.onTap,
             onTap: widget.onTap,
             onDoubleTap: widget.onDoubleTap,
             onDoubleTap: widget.onDoubleTap,
+            onTapDown: _onTapDown,
+            onTapUp: _onTapUp,
+            onTapCancel: _onCancel,
             child: menuItemWidget(context),
             child: menuItemWidget(context),
           );
           );
   }
   }
@@ -73,21 +98,25 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
     final borderRadius = Radius.circular(widget.borderRadius);
     final borderRadius = Radius.circular(widget.borderRadius);
     final isExpanded = widget.expandableController?.value;
     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)
         ? const Radius.circular(0)
         : borderRadius;
         : borderRadius;
     return AnimatedContainer(
     return AnimatedContainer(
-      duration: const Duration(milliseconds: 200),
+      duration: const Duration(milliseconds: 20),
       width: double.infinity,
       width: double.infinity,
-      padding: const EdgeInsets.symmetric(horizontal: 12),
+      padding: const EdgeInsets.only(left: 16, right: 12),
       decoration: BoxDecoration(
       decoration: BoxDecoration(
         borderRadius: BorderRadius.only(
         borderRadius: BorderRadius.only(
-          topLeft: borderRadius,
-          topRight: borderRadius,
+          topLeft: topBorderRadius,
+          topRight: topBorderRadius,
           bottomLeft: bottomBorderRadius,
           bottomLeft: bottomBorderRadius,
           bottomRight: bottomBorderRadius,
           bottomRight: bottomBorderRadius,
         ),
         ),
-        color: widget.menuItemColor,
+        color: menuItemColor,
       ),
       ),
       child: Row(
       child: Row(
         mainAxisAlignment: MainAxisAlignment.spaceBetween,
         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/ente_theme_data.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/text_style.dart';
 import 'package:photos/theme/text_style.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
 
 
 class NotificationWarningWidget extends StatelessWidget {
 class NotificationWarningWidget extends StatelessWidget {
   final IconData warningIcon;
   final IconData warningIcon;
@@ -33,8 +34,9 @@ class NotificationWarningWidget extends StatelessWidget {
               color: warning500,
               color: warning500,
             ),
             ),
             child: Padding(
             child: Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
               child: Row(
               child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
                 children: [
                   Icon(
                   Icon(
                     warningIcon,
                     warningIcon,
@@ -50,23 +52,14 @@ class NotificationWarningWidget extends StatelessWidget {
                     ),
                     ),
                   ),
                   ),
                   const SizedBox(width: 12),
                   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:flutter/material.dart';
 import 'package:photos/ente_theme_data.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 {
 class ToggleSwitchWidget extends StatefulWidget {
-  final bool value;
+  final ValueCallBack value;
   final OnChangedCallBack onChanged;
   final OnChangedCallBack onChanged;
   const ToggleSwitchWidget({
   const ToggleSwitchWidget({
     required this.value,
     required this.value,
@@ -17,24 +26,112 @@ class ToggleSwitchWidget extends StatefulWidget {
 }
 }
 
 
 class _ToggleSwitchWidgetState extends State<ToggleSwitchWidget> {
 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
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
     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:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/collection.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:photos/utils/toast_util.dart';
 import 'package:receive_sharing_intent/receive_sharing_intent.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) {
 String _actionName(CollectionActionType type, bool plural) {
   final titleSuffix = (plural ? "s" : "");
   final titleSuffix = (plural ? "s" : "");
@@ -38,6 +39,9 @@ String _actionName(CollectionActionType type, bool plural) {
     case CollectionActionType.restoreFiles:
     case CollectionActionType.restoreFiles:
       text = "Restore file";
       text = "Restore file";
       break;
       break;
+    case CollectionActionType.unHide:
+      text = "Unhide file";
+      break;
   }
   }
   return text + titleSuffix;
   return text + titleSuffix;
 }
 }
@@ -189,8 +193,16 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   }
   }
 
 
   Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
   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) {
     collectionsWithThumbnail.sort((first, second) {
       return compareAsciiLowerCaseNatural(
       return compareAsciiLowerCaseNatural(
         first.collection.name ?? "",
         first.collection.name ?? "",
@@ -273,6 +285,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
         return _addToCollection(collectionID);
         return _addToCollection(collectionID);
       case CollectionActionType.moveFiles:
       case CollectionActionType.moveFiles:
         return _moveFilesToCollection(collectionID);
         return _moveFilesToCollection(collectionID);
+      case CollectionActionType.unHide:
+        return _moveFilesToCollection(collectionID);
       case CollectionActionType.restoreFiles:
       case CollectionActionType.restoreFiles:
         return _restoreFilesToCollection(collectionID);
         return _restoreFilesToCollection(collectionID);
     }
     }
@@ -280,7 +294,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   }
   }
 
 
   Future<bool> _moveFilesToCollection(int toCollectionID) async {
   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();
     await dialog.show();
     try {
     try {
       final int fromCollectionID =
       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,
       extents: 1,
       onPageChanged: (index) async {
       onPageChanged: (index) async {
         await MemoriesService.instance.markMemoryAsSeen(widget.memories[index]);
         await MemoriesService.instance.markMemoryAsSeen(widget.memories[index]);
-        setState(() {
-          _index = index;
-        });
+        if (mounted) {
+          setState(() {
+            _index = index;
+          });
+        }
       },
       },
       physics: _shouldDisableScroll
       physics: _shouldDisableScroll
           ? const NeverScrollableScrollPhysics()
           ? 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:flutter/material.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/services/local_sync_service.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/ui/common/gradient_button.dart';
 import 'package:photos/utils/navigation_util.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
   @override
   Widget build(BuildContext context) {
   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/events/sync_status_update_event.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/user_remote_flag_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/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/home_header_widget.dart';
 import 'package:photos/ui/components/notification_warning_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';
 import 'package:photos/utils/navigation_util.dart';
 
 
 const double kContainerHeight = 36;
 const double kContainerHeight = 36;
@@ -84,9 +84,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
         HomeHeaderWidget(
         HomeHeaderWidget(
           centerWidget: _showStatus
           centerWidget: _showStatus
               ? _showErrorBanner
               ? _showErrorBanner
-                  ? const BrandTitleWidget(size: SizeVarient.medium)
+                  ? const Text("ente", style: brandStyleMedium)
                   : const SyncStatusWidget()
                   : const SyncStatusWidget()
-              : const BrandTitleWidget(size: SizeVarient.medium),
+              : const Text("ente", style: brandStyleMedium),
         ),
         ),
         AnimatedOpacity(
         AnimatedOpacity(
           opacity: _showErrorBanner ? 1 : 0,
           opacity: _showErrorBanner ? 1 : 0,
@@ -100,9 +100,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
             : const SizedBox.shrink(),
             : const SizedBox.shrink(),
         UserRemoteFlagService.instance.shouldShowRecoveryVerification()
         UserRemoteFlagService.instance.shouldShowRecoveryVerification()
             ? NotificationWarningWidget(
             ? NotificationWarningWidget(
-                warningIcon: Icons.gpp_maybe,
+                warningIcon: Icons.error_outline,
                 actionIcon: Icons.arrow_forward,
                 actionIcon: Icons.arrow_forward,
-                text: "Please ensure you have your 24 word recovery key",
+                text: "Confirm your recovery key",
                 onTap: () async => {
                 onTap: () async => {
                   await routeToPage(
                   await routeToPage(
                     context,
                     context,

+ 51 - 402
lib/ui/home_widget.dart

@@ -2,62 +2,48 @@
 
 
 import 'dart:async';
 import 'dart:async';
 import 'dart:io';
 import 'dart:io';
-import 'dart:ui';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:move_to_background/move_to_background.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/configuration.dart';
 import 'package:photos/core/event_bus.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/ente_theme_data.dart';
 import 'package:photos/events/account_configured_event.dart';
 import 'package:photos/events/account_configured_event.dart';
 import 'package:photos/events/backup_folders_updated_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/permission_granted_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/events/trigger_logout_event.dart';
 import 'package:photos/events/trigger_logout_event.dart';
 import 'package:photos/events/user_logged_out_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/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.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/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.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/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/collections_gallery_widget.dart';
 import 'package:photos/ui/common/bottom_shadow.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/create_collection_page.dart';
 import 'package:photos/ui/extents_page_view.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/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/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
 import 'package:photos/ui/settings_page.dart';
 import 'package:photos/ui/shared_collections_gallery.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/ui/viewer/gallery/gallery_overlay_widget.dart';
 import 'package:photos/utils/dialog_util.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:receive_sharing_intent/receive_sharing_intent.dart';
 import 'package:uni_links/uni_links.dart';
 import 'package:uni_links/uni_links.dart';
 
 
@@ -81,7 +67,6 @@ class _HomeWidgetState extends State<HomeWidget> {
 
 
   final PageController _pageController = PageController();
   final PageController _pageController = PageController();
   int _selectedTabIndex = 0;
   int _selectedTabIndex = 0;
-  Widget _headerWidgetWithSettingsButton;
 
 
   // for receiving media files
   // for receiving media files
   // ignore: unused_field
   // ignore: unused_field
@@ -100,15 +85,14 @@ class _HomeWidgetState extends State<HomeWidget> {
   @override
   @override
   void initState() {
   void initState() {
     _logger.info("Building initstate");
     _logger.info("Building initstate");
-    _headerWidgetWithSettingsButton = Stack(
-      children: const [
-        _headerWidget,
-      ],
-    );
     _tabChangedEventSubscription =
     _tabChangedEventSubscription =
         Bus.instance.on<TabChangedEvent>().listen((event) {
         Bus.instance.on<TabChangedEvent>().listen((event) {
       if (event.source != TabChangedEventSource.pageView) {
       if (event.source != TabChangedEventSource.pageView) {
+        debugPrint(
+          "TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}",
+        );
         _selectedTabIndex = event.selectedIndex;
         _selectedTabIndex = event.selectedIndex;
+        // _pageController.jumpToPage(_selectedTabIndex);
         _pageController.animateToPage(
         _pageController.animateToPage(
           event.selectedIndex,
           event.selectedIndex,
           duration: const Duration(milliseconds: 100),
           duration: const Duration(milliseconds: 100),
@@ -126,34 +110,7 @@ class _HomeWidgetState extends State<HomeWidget> {
     });
     });
     _triggerLogoutEvent =
     _triggerLogoutEvent =
         Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
         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) {
     _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
       _logger.info('logged out, selectTab index to 0');
       _logger.info('logged out, selectTab index to 0');
@@ -218,6 +175,37 @@ class _HomeWidgetState extends State<HomeWidget> {
     super.initState();
     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
   @override
   void dispose() {
   void dispose() {
     _tabChangedEventSubscription.cancel();
     _tabChangedEventSubscription.cancel();
@@ -262,8 +250,8 @@ class _HomeWidgetState extends State<HomeWidget> {
       child: WillPopScope(
       child: WillPopScope(
         child: Scaffold(
         child: Scaffold(
           drawerScrimColor: getEnteColorScheme(context).strokeFainter,
           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
           drawer: enableDrawer
               ? ConstrainedBox(
               ? ConstrainedBox(
                   constraints: const BoxConstraints(maxWidth: 428),
                   constraints: const BoxConstraints(maxWidth: 428),
@@ -345,8 +333,12 @@ class _HomeWidgetState extends State<HomeWidget> {
               physics: const BouncingScrollPhysics(),
               physics: const BouncingScrollPhysics(),
               children: [
               children: [
                 showBackupFolderHook
                 showBackupFolderHook
-                    ? _getBackupFolderSelectionHook()
-                    : _getMainGalleryWidget(),
+                    ? const StartBackupHookWidget(headerWidget: _headerWidget)
+                    : HomeGalleryWidget(
+                        header: _headerWidget,
+                        footer: const PreserveFooterWidget(),
+                        selectedFiles: _selectedFiles,
+                      ),
                 _deviceFolderGalleryWidget,
                 _deviceFolderGalleryWidget,
                 _sharedCollectionGallery,
                 _sharedCollectionGallery,
               ],
               ],
@@ -422,347 +414,4 @@ class _HomeWidgetState extends State<HomeWidget> {
     final ott = Uri.parse(link).queryParameters["ott"];
     final ott = Uri.parse(link).queryParameters["ott"];
     UserService.instance.verifyEmail(context, 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 EdgeInsetsGeometry padding;
   final int totalCount;
   final int totalCount;
   final int initialScrollIndex;
   final int initialScrollIndex;
+  final double bottomSafeArea;
   final int currentFirstIndex;
   final int currentFirstIndex;
   final ValueChanged<double> onChange;
   final ValueChanged<double> onChange;
   final String Function(int) labelTextBuilder;
   final String Function(int) labelTextBuilder;
@@ -26,6 +27,7 @@ class DraggableScrollbar extends StatefulWidget {
     this.backgroundColor = Colors.white,
     this.backgroundColor = Colors.white,
     this.drawColor = Colors.grey,
     this.drawColor = Colors.grey,
     this.heightScrollThumb = 80.0,
     this.heightScrollThumb = 80.0,
+    this.bottomSafeArea = 120,
     this.padding,
     this.padding,
     this.totalCount = 1,
     this.totalCount = 1,
     this.initialScrollIndex = 0,
     this.initialScrollIndex = 0,
@@ -49,7 +51,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
 
 
   double get thumbMin => 0.0;
   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;
   AnimationController _thumbAnimationController;
   Animation<double> _thumbAnimation;
   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.
   /// Height of scroll thumb, defaults to 48.
   final double thumbHeight;
   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].
   /// Called to build an individual item with the specified [index].
   final HugeListViewItemBuilder<T> itemBuilder;
   final HugeListViewItemBuilder<T> itemBuilder;
 
 
@@ -72,6 +76,7 @@ class HugeListView<T> extends StatefulWidget {
     this.thumbBackgroundColor = Colors.red, // Colors.white,
     this.thumbBackgroundColor = Colors.red, // Colors.white,
     this.thumbDrawColor = Colors.yellow, //Colors.grey,
     this.thumbDrawColor = Colors.yellow, //Colors.grey,
     this.thumbHeight = 48.0,
     this.thumbHeight = 48.0,
+    this.bottomSafeArea = 120.0,
     this.isDraggableScrollbarEnabled = true,
     this.isDraggableScrollbarEnabled = true,
     this.thumbPadding,
     this.thumbPadding,
   }) : super(key: key);
   }) : super(key: key);
@@ -83,6 +88,7 @@ class HugeListView<T> extends StatefulWidget {
 class HugeListViewState<T> extends State<HugeListView<T>> {
 class HugeListViewState<T> extends State<HugeListView<T>> {
   final scrollKey = GlobalKey<DraggableScrollbarState>();
   final scrollKey = GlobalKey<DraggableScrollbarState>();
   final listener = ItemPositionsListener.create();
   final listener = ItemPositionsListener.create();
+  int lastIndexJump = -1;
   dynamic error;
   dynamic error;
 
 
   @override
   @override
@@ -131,13 +137,27 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
           totalCount: widget.totalCount,
           totalCount: widget.totalCount,
           initialScrollIndex: widget.startIndex,
           initialScrollIndex: widget.startIndex,
           onChange: (position) {
           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,
           labelTextBuilder: widget.labelTextBuilder,
           backgroundColor: widget.thumbBackgroundColor,
           backgroundColor: widget.thumbBackgroundColor,
           drawColor: widget.thumbDrawColor,
           drawColor: widget.thumbDrawColor,
           heightScrollThumb: widget.thumbHeight,
           heightScrollThumb: widget.thumbHeight,
+          bottomSafeArea: widget.bottomSafeArea,
           currentFirstIndex: _currentFirst(),
           currentFirstIndex: _currentFirst(),
           isEnabled: widget.isDraggableScrollbarEnabled,
           isEnabled: widget.isDraggableScrollbarEnabled,
           padding: widget.thumbPadding,
           padding: widget.thumbPadding,

+ 1 - 15
lib/ui/nav_bar.dart

@@ -2,8 +2,6 @@
 
 
 library google_nav_bar;
 library google_nav_bar;
 
 
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 
 
@@ -120,19 +118,7 @@ class _GNavState extends State<GNav> {
                     Colors.transparent,
                     Colors.transparent,
                 duration: widget.duration ?? const Duration(milliseconds: 500),
                 duration: widget.duration ?? const Duration(milliseconds: 500),
                 onPressed: () {
                 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
           BillingService.instance
               .verifySubscription(freeProductID, "", paymentProvider: "ente");
               .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 'dart:async';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/subscription.dart';
 import 'package:photos/models/subscription.dart';
@@ -38,7 +37,6 @@ class StripeSubscriptionPage extends StatefulWidget {
 }
 }
 
 
 class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
 class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
-  final _logger = Logger("StripeSubscriptionPage");
   final _billingService = BillingService.instance;
   final _billingService = BillingService.instance;
   final _userService = UserService.instance;
   final _userService = UserService.instance;
   Subscription _currentSubscription;
   Subscription _currentSubscription;

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

@@ -94,7 +94,7 @@ class ValidityWidget extends StatelessWidget {
     );
     );
     var message = "Renews on $endDate";
     var message = "Renews on $endDate";
     if (currentSubscription.productID == freeProductID) {
     if (currentSubscription.productID == freeProductID) {
-      message = "Free plan valid till $endDate";
+      message = "Free trial valid till $endDate";
     } else if (currentSubscription.attributes?.isCancelled ?? false) {
     } else if (currentSubscription.attributes?.isCancelled ?? false) {
       message = "Your subscription will be cancelled on $endDate";
       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(
       planWidgets.add(
         SubscriptionPlanWidget(
         SubscriptionPlanWidget(
           storage: _freePlan.storage,
           storage: _freePlan.storage,
-          price: "free",
+          price: "Free trial",
           period: "",
           period: "",
           isActive: true,
           isActive: true,
         ),
         ),

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

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

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

@@ -2,6 +2,7 @@
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.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/common/web_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.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/expandable_menu_item_widget.dart';
@@ -47,6 +48,7 @@ class AboutSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Source code",
             title: "Source code",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -61,6 +63,7 @@ class AboutSectionWidget extends StatelessWidget {
                     captionedTextWidget: const CaptionedTextWidget(
                     captionedTextWidget: const CaptionedTextWidget(
                       title: "Check for updates",
                       title: "Check for updates",
                     ),
                     ),
+                    pressedColor: getEnteColorScheme(context).fillFaint,
                     trailingIcon: Icons.chevron_right_outlined,
                     trailingIcon: Icons.chevron_right_outlined,
                     trailingIconIsMuted: true,
                     trailingIconIsMuted: true,
                     onTap: () async {
                     onTap: () async {
@@ -111,6 +114,7 @@ class AboutMenuItemWidget extends StatelessWidget {
       captionedTextWidget: CaptionedTextWidget(
       captionedTextWidget: CaptionedTextWidget(
         title: title,
         title: title,
       ),
       ),
+      pressedColor: getEnteColorScheme(context).fillFaint,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIconIsMuted: true,
       trailingIconIsMuted: true,
       onTap: () async {
       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:flutter_sodium/flutter_sodium.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_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/change_email_dialog.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/recovery_key_page.dart';
 import 'package:photos/ui/account/recovery_key_page.dart';
@@ -34,6 +35,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Recovery key",
             title: "Recovery key",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -67,6 +69,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Change email",
             title: "Change email",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -92,6 +95,7 @@ class AccountSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Change password",
             title: "Change password",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {

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

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

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

@@ -3,18 +3,17 @@
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/duplicate_files.dart';
 import 'package:photos/models/duplicate_files.dart';
 import 'package:photos/services/deduplication_service.dart';
 import 'package:photos/services/deduplication_service.dart';
 import 'package:photos/services/sync_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/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/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_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/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/settings/common_settings.dart';
 import 'package:photos/ui/tools/deduplicate_page.dart';
 import 'package:photos/ui/tools/deduplicate_page.dart';
 import 'package:photos/ui/tools/free_space_page.dart';
 import 'package:photos/ui/tools/free_space_page.dart';
@@ -48,6 +47,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
         captionedTextWidget: const CaptionedTextWidget(
         captionedTextWidget: const CaptionedTextWidget(
           title: "Backed up folders",
           title: "Backed up folders",
         ),
         ),
+        pressedColor: getEnteColorScheme(context).fillFaint,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIconIsMuted: true,
         trailingIconIsMuted: true,
         onTap: () {
         onTap: () {
@@ -62,66 +62,28 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
       sectionOptionSpacing,
       sectionOptionSpacing,
       MenuItemWidget(
       MenuItemWidget(
         captionedTextWidget: const CaptionedTextWidget(
         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,
       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(
     sectionOptions.addAll(
       [
       [
         MenuItemWidget(
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Free up space",
             title: "Free up space",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -157,6 +119,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Deduplicate files",
             title: "Deduplicate files",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {

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

@@ -3,6 +3,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/user_service.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/account/delete_account_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.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/expandable_menu_item_widget.dart';
@@ -30,6 +31,7 @@ class DangerSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Logout",
             title: "Logout",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () {
           onTap: () {
@@ -41,6 +43,7 @@ class DangerSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Delete account",
             title: "Delete account",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () {
           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/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/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/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_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/menu_item_widget.dart';
@@ -32,6 +33,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Key attributes",
             title: "Key attributes",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -43,6 +45,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Delete Local Import DB",
             title: "Delete Local Import DB",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -55,6 +58,7 @@ class DebugSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Allow auto-upload for ignored files",
             title: "Allow auto-upload for ignored files",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           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/events/two_factor_status_change_event.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_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/account/sessions_page.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
@@ -72,8 +73,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                 ),
                 ),
                 trailingSwitch: snapshot.hasData
                 trailingSwitch: snapshot.hasData
                     ? ToggleSwitchWidget(
                     ? ToggleSwitchWidget(
-                        value: snapshot.data,
-                        onChanged: (value) async {
+                        value: () => snapshot.data,
+                        onChanged: () async {
                           final hasAuthenticated =
                           final hasAuthenticated =
                               await LocalAuthenticationService.instance
                               await LocalAuthenticationService.instance
                                   .requestLocalAuthentication(
                                   .requestLocalAuthentication(
@@ -81,7 +82,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                             "Please authenticate to configure two-factor authentication",
                             "Please authenticate to configure two-factor authentication",
                           );
                           );
                           if (hasAuthenticated) {
                           if (hasAuthenticated) {
-                            if (value) {
+                            if (!snapshot.data) {
                               UserService.instance.setupTwoFactor(context);
                               UserService.instance.setupTwoFactor(context);
                             } else {
                             } else {
                               _disableTwoFactor();
                               _disableTwoFactor();
@@ -105,18 +106,15 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
           title: "Lockscreen",
           title: "Lockscreen",
         ),
         ),
         trailingSwitch: ToggleSwitchWidget(
         trailingSwitch: ToggleSwitchWidget(
-          value: _config.shouldShowLockScreen(),
-          onChanged: (value) async {
-            final hasAuthenticated = await LocalAuthenticationService.instance
+          value: () => _config.shouldShowLockScreen(),
+          onChanged: () async {
+            await LocalAuthenticationService.instance
                 .requestLocalAuthForLockScreen(
                 .requestLocalAuthForLockScreen(
               context,
               context,
-              value,
+              !_config.shouldShowLockScreen(),
               "Please authenticate to change lockscreen setting",
               "Please authenticate to change lockscreen setting",
               "To enable lockscreen, please setup device passcode or screen lock in your system settings.",
               "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",
               title: "Hide from recents",
             ),
             ),
             trailingSwitch: ToggleSwitchWidget(
             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,
           sectionOptionSpacing,
@@ -216,6 +141,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
         captionedTextWidget: const CaptionedTextWidget(
         captionedTextWidget: const CaptionedTextWidget(
           title: "Active sessions",
           title: "Active sessions",
         ),
         ),
+        pressedColor: getEnteColorScheme(context).fillFaint,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIconIsMuted: true,
         trailingIconIsMuted: true,
         onTap: () async {
         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',
                         ' on tree',
                   );
                   );
                   throw Error();
                   throw Error();
-                }
-                if (snapshot.hasData) {
+                } else if (snapshot.hasData) {
                   final userDetails = snapshot.data as UserDetails;
                   final userDetails = snapshot.data as UserDetails;
                   return Text(
                   return Text(
                     "${NumberFormat().format(userDetails.fileCount)} memories",
                     "${NumberFormat().format(userDetails.fileCount)} memories",
                     style: getEnteTextTheme(context).largeBold,
                     style: getEnteTextTheme(context).largeBold,
                   );
                   );
-                }
-                if (snapshot.hasError) {
+                } else if (snapshot.hasError) {
                   logger.severe('failed to load user details');
                   logger.severe('failed to load user details');
                   return const EnteLoadingWidget();
                   return const EnteLoadingWidget();
                 } else {
                 } else {

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

@@ -4,6 +4,7 @@ import 'dart:io';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.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/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_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/menu_item_widget.dart';
@@ -61,6 +62,7 @@ class SocialsMenuItemWidget extends StatelessWidget {
       captionedTextWidget: CaptionedTextWidget(
       captionedTextWidget: CaptionedTextWidget(
         title: text,
         title: text,
       ),
       ),
+      pressedColor: getEnteColorScheme(context).fillFaint,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIconIsMuted: true,
       trailingIconIsMuted: true,
       onTap: () {
       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:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.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/common/web_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.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/expandable_menu_item_widget.dart';
@@ -34,6 +35,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Email",
             title: "Email",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
@@ -45,6 +47,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Roadmap",
             title: "Roadmap",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () {
           onTap: () {
@@ -67,6 +70,7 @@ class SupportSectionWidget extends StatelessWidget {
           captionedTextWidget: const CaptionedTextWidget(
           captionedTextWidget: const CaptionedTextWidget(
             title: "Report a bug",
             title: "Report a bug",
           ),
           ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           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:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 import 'package:photos/ente_theme_data.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/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_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/menu_item_widget.dart';
@@ -69,7 +70,8 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
         title: toBeginningOfSentenceCase(themeMode.name),
         title: toBeginningOfSentenceCase(themeMode.name),
         textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
         textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
       ),
       ),
-      isHeaderOfExpansion: false,
+      pressedColor: getEnteColorScheme(context).fillFaint,
+      isExpandable: false,
       trailingIcon: currentThemeMode == themeMode ? Icons.check : null,
       trailingIcon: currentThemeMode == themeMode ? Icons.check : null,
       onTap: () async {
       onTap: () async {
         AdaptiveTheme.of(context).setThemeMode(themeMode);
         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/backup_section_widget.dart';
 import 'package:photos/ui/settings/danger_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/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/security_section_widget.dart';
 import 'package:photos/ui/settings/settings_title_bar_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/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/support_section_widget.dart';
 import 'package:photos/ui/settings/theme_switch_widget.dart';
 import 'package:photos/ui/settings/theme_switch_widget.dart';
 
 
@@ -42,6 +42,7 @@ class SettingsPage extends StatelessWidget {
     final List<Widget> contents = [];
     final List<Widget> contents = [];
     contents.add(
     contents.add(
       Container(
       Container(
+        constraints: const BoxConstraints(maxWidth: 350),
         padding: const EdgeInsets.symmetric(horizontal: 8),
         padding: const EdgeInsets.symmetric(horizontal: 8),
         child: Align(
         child: Align(
           alignment: Alignment.centerLeft,
           alignment: Alignment.centerLeft,
@@ -65,7 +66,7 @@ class SettingsPage extends StatelessWidget {
     contents.add(const SizedBox(height: 8));
     contents.add(const SizedBox(height: 8));
     if (hasLoggedIn) {
     if (hasLoggedIn) {
       contents.addAll([
       contents.addAll([
-        const DetailsSectionWidget(),
+        const StorageCardWidget(),
         const SizedBox(height: 12),
         const SizedBox(height: 12),
         const BackupSectionWidget(),
         const BackupSectionWidget(),
         sectionSpacing,
         sectionSpacing,

+ 2 - 2
lib/ui/shared_collections_gallery.dart

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

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

@@ -1,6 +1,7 @@
 // @dart=2.9
 // @dart=2.9
 
 
 import 'dart:io';
 import 'dart:io';
+import 'dart:math';
 import 'dart:typed_data';
 import 'dart:typed_data';
 
 
 import 'package:extended_image/extended_image.dart';
 import 'package:extended_image/extended_image.dart';
@@ -370,13 +371,19 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
         existingFiles[0].creationTime,
         existingFiles[0].creationTime,
       ))
       ))
           .files;
           .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(
       replacePage(
         context,
         context,
         DetailPage(
         DetailPage(
           widget.detailPageConfig.copyWith(
           widget.detailPageConfig.copyWith(
             files: files,
             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:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
@@ -11,6 +12,7 @@ import 'package:photos/utils/navigation_util.dart';
 
 
 class CollectionsListOfFileWidget extends StatelessWidget {
 class CollectionsListOfFileWidget extends StatelessWidget {
   final Future<Set<int>> allCollectionIDsOfFile;
   final Future<Set<int>> allCollectionIDsOfFile;
+
   const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
   const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
       : super(key: key);
       : super(key: key);
 
 
@@ -21,19 +23,23 @@ class CollectionsListOfFileWidget extends StatelessWidget {
       builder: (context, snapshot) {
       builder: (context, snapshot) {
         if (snapshot.hasData) {
         if (snapshot.hasData) {
           final Set<int> collectionIDs = snapshot.data;
           final Set<int> collectionIDs = snapshot.data;
-          final collections = [];
+          final collections = <Collection>[];
           for (var collectionID in collectionIDs) {
           for (var collectionID in collectionIDs) {
-            collections.add(
-              CollectionsService.instance.getCollectionByID(collectionID),
-            );
+            final c =
+                CollectionsService.instance.getCollectionByID(collectionID);
+            collections.add(c);
           }
           }
           return ListView.builder(
           return ListView.builder(
             itemCount: collections.length,
             itemCount: collections.length,
             scrollDirection: Axis.horizontal,
             scrollDirection: Axis.horizontal,
             itemBuilder: (context, index) {
             itemBuilder: (context, index) {
+              final bool isHidden = collections[index].isHidden();
               return FileInfoCollectionWidget(
               return FileInfoCollectionWidget(
-                name: collections[index].name,
+                name: isHidden ? 'Hidden' : collections[index].name,
                 onTap: () {
                 onTap: () {
+                  if (isHidden) {
+                    return;
+                  }
                   routeToPage(
                   routeToPage(
                     context,
                     context,
                     CollectionPage(
                     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:like_button/like_button.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:media_extension/media_extension.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:path/path.dart' as file_path;
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/event_bus.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.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/ignored_file.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/models/trash_file.dart';
+import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/favorites_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/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/ui/common/progress_dialog.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/ui/viewer/file/custom_app_bar.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -99,11 +104,21 @@ class FadingAppBarState extends State<FadingAppBar> {
 
 
   AppBar _buildAppBar() {
   AppBar _buildAppBar() {
     debugPrint("building app bar");
     debugPrint("building app bar");
+
     final List<Widget> actions = [];
     final List<Widget> actions = [];
     final isTrashedFile = widget.file is TrashFile;
     final isTrashedFile = widget.file is TrashFile;
     final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
     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
     // 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(_getFavoriteButton());
     }
     }
     actions.add(
     actions.add(
@@ -132,8 +147,7 @@ class FadingAppBarState extends State<FadingAppBar> {
             );
             );
           }
           }
           // options for files owned by the user
           // options for files owned by the user
-          if (widget.file.ownerID == null ||
-              widget.file.ownerID == widget.userID) {
+          if (isOwnedByUser) {
             items.add(
             items.add(
               PopupMenuItem(
               PopupMenuItem(
                 value: 2,
                 value: 2,
@@ -169,12 +183,51 @@ class FadingAppBarState extends State<FadingAppBar> {
                     const Padding(
                     const Padding(
                       padding: EdgeInsets.all(8),
                       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;
           return items;
         },
         },
         onSelected: (value) {
         onSelected: (value) {
@@ -184,6 +237,10 @@ class FadingAppBarState extends State<FadingAppBar> {
             _showDeleteSheet(widget.file);
             _showDeleteSheet(widget.file);
           } else if (value == 3) {
           } else if (value == 3) {
             _setAs(widget.file);
             _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() {
   Widget _getFavoriteButton() {
     return FutureBuilder(
     return FutureBuilder(
       future: FavoritesService.instance.isFavorite(widget.file),
       future: FavoritesService.instance.isFavorite(widget.file),
@@ -326,55 +415,97 @@ class FadingAppBarState extends State<FadingAppBar> {
   Future<void> _download(File file) async {
   Future<void> _download(File file) async {
     final dialog = createProgressDialog(context, "Downloading...");
     final dialog = createProgressDialog(context, "Downloading...");
     await dialog.show();
     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(
         final ignoreVideoFile = IgnoredFile(
           savedAsset.id,
           savedAsset.id,
-          savedAsset.title ?? videoTitle,
+          savedAsset.title ?? "",
           savedAsset.relativePath ?? 'remoteDownload',
           savedAsset.relativePath ?? 'remoteDownload',
           "remoteDownload",
           "remoteDownload",
         );
         );
         debugPrint("IgnoreFile for auto-upload ${ignoreVideoFile.toString()}");
         debugPrint("IgnoreFile for auto-upload ${ignoreVideoFile.toString()}");
         await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
         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");
       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 {
   Future<void> _setAs(File file) async {
     final dialog = createProgressDialog(context, "Please wait...");
     final dialog = createProgressDialog(context, "Please wait...");
     await dialog.show();
     await dialog.show();
     try {
     try {
       final io.File fileToSave = await getFile(file);
       final io.File fileToSave = await getFile(file);
-      var m = MediaExtension();
+      final m = MediaExtension();
       final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
       final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
       if (result == false) {
       if (result == false) {
         showShortToast(context, "Something went wrong");
         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/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/file.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/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_file.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/create_collection_page.dart';
 import 'package:photos/ui/viewer/file/file_info_widget.dart';
 import 'package:photos/ui/viewer/file/file_info_widget.dart';
 import 'package:photos/utils/delete_file_util.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,
               Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
               color: Colors.white,
               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) {
     if (widget.file is TrashFile) {
       _addTrashOptions(children);
       _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.showOnlyInfoButton && widget.file is! TrashFile) {
       if (widget.file.fileType == FileType.image ||
       if (widget.file.fileType == FileType.image ||
           widget.file.fileType == FileType.livePhoto) {
           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 =
         final bool isArchived =
             widget.file.magicMetadata.visibility == visibilityArchive;
             widget.file.magicMetadata.visibility == visibilityArchive;
         children.add(
         children.add(
           Tooltip(
           Tooltip(
-            message: isArchived ? "Unhide" : "Hide",
+            message: isArchived ? "Unarchive" : "Archive",
             child: Padding(
             child: Padding(
               padding: const EdgeInsets.only(top: 12, bottom: 12),
               padding: const EdgeInsets.only(top: 12, bottom: 12),
               child: IconButton(
               child: IconButton(
                 icon: Icon(
                 icon: Icon(
-                  isArchived
-                      ? Icons.visibility_outlined
-                      : Icons.visibility_off_outlined,
+                  isArchived ? Icons.unarchive : Icons.archive_outlined,
                   color: Colors.white,
                   color: Colors.white,
                 ),
                 ),
                 onPressed: () async {
                 onPressed: () async {
@@ -176,9 +191,31 @@ class FadingBottomBarState extends State<FadingBottomBar> {
             ),
             ),
             child: Padding(
             child: Padding(
               padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
               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 {
   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,
       context: context,
-      isScrollControlled: true,
       builder: (BuildContext context) {
       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/ente_theme_data.dart";
 import "package:photos/models/file.dart";
 import "package:photos/models/file.dart";
 import "package:photos/models/file_type.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/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/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/date_time_util.dart";
 import "package:photos/utils/exif_util.dart";
 import "package:photos/utils/exif_util.dart";
 import "package:photos/utils/file_util.dart";
 import "package:photos/utils/file_util.dart";
@@ -51,9 +54,11 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
         widget.file.fileType == FileType.livePhoto;
         widget.file.fileType == FileType.livePhoto;
     if (_isImage) {
     if (_isImage) {
       getExif(widget.file).then((exif) {
       getExif(widget.file).then((exif) {
-        setState(() {
-          _exif = exif;
-        });
+        if (mounted) {
+          setState(() {
+            _exif = exif;
+          });
+        }
       });
       });
     }
     }
     super.initState();
     super.initState();
@@ -88,9 +93,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
     final bool showDimension =
     final bool showDimension =
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
     final listTiles = <Widget>[
     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(
       ListTile(
+        horizontalTitleGap: 2,
         leading: const Padding(
         leading: const Padding(
-          padding: EdgeInsets.only(top: 8, left: 6),
+          padding: EdgeInsets.only(top: 8),
           child: Icon(Icons.calendar_today_rounded),
           child: Icon(Icons.calendar_today_rounded),
         ),
         ),
         title: Text(
         title: Text(
@@ -119,17 +132,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               )
               )
             : const SizedBox.shrink(),
             : const SizedBox.shrink(),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       ListTile(
       ListTile(
+        horizontalTitleGap: 2,
         leading: _isImage
         leading: _isImage
             ? const Padding(
             ? const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                 child: Icon(
                   Icons.image,
                   Icons.image,
                 ),
                 ),
               )
               )
             : const Padding(
             : const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                 child: Icon(
                   Icons.video_camera_back,
                   Icons.video_camera_back,
                   size: 27,
                   size: 27,
@@ -167,13 +180,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                 icon: const Icon(Icons.edit),
                 icon: const Icon(Icons.edit),
               ),
               ),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       showExifListTile
       showExifListTile
           ? ListTile(
           ? 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"] ?? "--"),
               title: Text(_exifData["takenOnDevice"] ?? "--"),
               subtitle: Row(
               subtitle: Row(
                 children: [
                 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(
       SizedBox(
         height: 62,
         height: 62,
         child: ListTile(
         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
           title: fileIsBackedup
               ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
               ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
         ),
         ),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       (file.uploadedFileID != null && file.updationTime != null)
       (file.uploadedFileID != null && file.updationTime != null)
           ? ListTile(
           ? ListTile(
+              horizontalTitleGap: 2,
               leading: const Padding(
               leading: const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(Icons.cloud_upload_outlined),
                 child: Icon(Icons.cloud_upload_outlined),
               ),
               ),
               title: Text(
               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)
           .getFileSize(widget.file.uploadedFileID)
           .then((value) {
           .then((value) {
         widget.file.fileSize = 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),
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
         child: GalleryAppBarWidget(
           appBarType,
           appBarType,
-          "Hidden",
+          "Archive",
           _selectedFiles,
           _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/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/ignored_files_service.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.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
@@ -19,16 +20,21 @@ class CollectionPage extends StatelessWidget {
   final String tagPrefix;
   final String tagPrefix;
   final GalleryType appBarType;
   final GalleryType appBarType;
   final _selectedFiles = SelectedFiles();
   final _selectedFiles = SelectedFiles();
+  bool hasVerifiedLock;
 
 
   CollectionPage(
   CollectionPage(
     this.c, {
     this.c, {
     this.tagPrefix = "collection",
     this.tagPrefix = "collection",
     this.appBarType = GalleryType.ownedCollection,
     this.appBarType = GalleryType.ownedCollection,
+    this.hasVerifiedLock = false,
     Key key,
     Key key,
   }) : super(key: key);
   }) : super(key: key);
 
 
   @override
   @override
   Widget build(Object context) {
   Widget build(Object context) {
+    if (hasVerifiedLock == false && c.collection.isHidden()) {
+      return const EmptyState();
+    }
     final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
     final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
     final gallery = Gallery(
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
@@ -55,6 +61,7 @@ class CollectionPage extends StatelessWidget {
       removalEventTypes: const {
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
         EventType.deletedFromEverywhere,
+        EventType.hide,
       },
       },
       tagPrefix: tagPrefix,
       tagPrefix: tagPrefix,
       selectedFiles: _selectedFiles,
       selectedFiles: _selectedFiles,

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است