Browse Source

Merge branch 'main' into cache_info

Neeraj Gupta 2 years ago
parent
commit
20514e238d
93 changed files with 4989 additions and 1045 deletions
  1. 10 5
      README.md
  2. 1 1
      fastlane/metadata/android/en-US/full_description.txt
  3. 7 0
      lib/core/constants.dart
  4. 0 12
      lib/db/files_db.dart
  5. 3 0
      lib/ente_theme_data.dart
  6. 12 0
      lib/extensions/string_ext.dart
  7. 3 3
      lib/models/api/collection/create_request.dart
  8. 72 7
      lib/models/collection.dart
  9. 150 0
      lib/models/gallery_type.dart
  10. 9 1
      lib/models/magic_metadata.dart
  11. 16 0
      lib/models/selected_file_breakup.dart
  12. 39 5
      lib/models/selected_files.dart
  13. 1 12
      lib/models/user_details.dart
  14. 67 9
      lib/services/collections_service.dart
  15. 23 2
      lib/services/favorites_service.dart
  16. 2 1
      lib/services/hidden_service.dart
  17. 1 1
      lib/services/remote_sync_service.dart
  18. 1 1
      lib/services/update_service.dart
  19. 1 1
      lib/services/user_service.dart
  20. 11 20
      lib/states/user_details_state.dart
  21. 72 10
      lib/theme/colors.dart
  22. 14 4
      lib/theme/ente_theme.dart
  23. 6 10
      lib/ui/account/recovery_key_page.dart
  24. 1 1
      lib/ui/account/two_factor_setup_page.dart
  25. 134 0
      lib/ui/actions/collection/collection_file_actions.dart
  26. 245 0
      lib/ui/actions/collection/collection_sharing_actions.dart
  27. 11 1
      lib/ui/collections/collection_item_widget.dart
  28. 7 1
      lib/ui/common/gradient_button.dart
  29. 4 3
      lib/ui/common/loading_widget.dart
  30. 183 0
      lib/ui/components/action_sheet_widget.dart
  31. 120 0
      lib/ui/components/blur_menu_item_widget.dart
  32. 124 0
      lib/ui/components/bottom_action_bar/action_bar_widget.dart
  33. 184 0
      lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart
  34. 71 0
      lib/ui/components/bottom_action_bar/expanded_menu_widget.dart
  35. 425 0
      lib/ui/components/button_widget.dart
  36. 1 1
      lib/ui/components/captioned_text_widget.dart
  37. 146 0
      lib/ui/components/dialog_widget.dart
  38. 1 1
      lib/ui/components/expandable_menu_item_widget.dart
  39. 0 3
      lib/ui/components/home_header_widget.dart
  40. 18 4
      lib/ui/components/menu_item_widget.dart
  41. 1 0
      lib/ui/components/menu_section_description_widget.dart
  42. 35 0
      lib/ui/components/menu_section_title.dart
  43. 203 0
      lib/ui/components/models/button_type.dart
  44. 33 0
      lib/ui/components/models/custom_button_style.dart
  45. 6 1
      lib/ui/create_collection_page.dart
  46. 0 3
      lib/ui/extents_page_view.dart
  47. 9 1
      lib/ui/home/home_gallery_widget.dart
  48. 1 1
      lib/ui/home/landing_page_widget.dart
  49. 2 7
      lib/ui/home_widget.dart
  50. 26 7
      lib/ui/huge_listview/lazy_loading_gallery.dart
  51. 17 15
      lib/ui/huge_listview/scroll_bar_thumb.dart
  52. 1 1
      lib/ui/notification/prompts/password_reminder.dart
  53. 50 82
      lib/ui/notification/update/change_log_page.dart
  54. 2 0
      lib/ui/payment/stripe_subscription_page.dart
  55. 1 1
      lib/ui/settings/about_section_widget.dart
  56. 1 1
      lib/ui/settings/common_settings.dart
  57. 5 23
      lib/ui/settings/settings_title_bar_widget.dart
  58. 6 8
      lib/ui/settings/social_section_widget.dart
  59. 12 21
      lib/ui/settings/storage_card_widget.dart
  60. 4 1
      lib/ui/settings_page.dart
  61. 13 17
      lib/ui/shared_collections_gallery.dart
  62. 332 0
      lib/ui/sharing/add_partipant_page.dart
  63. 275 0
      lib/ui/sharing/album_participants_page.dart
  64. 163 0
      lib/ui/sharing/manage_album_participant.dart
  65. 32 6
      lib/ui/sharing/manage_links_widget.dart
  66. 333 0
      lib/ui/sharing/share_collection_page.dart
  67. 0 494
      lib/ui/sharing/share_collection_widget.dart
  68. 81 0
      lib/ui/sharing/user_avator_widget.dart
  69. 17 13
      lib/ui/tools/debug/cache_size_view.dart
  70. 36 30
      lib/ui/tools/debug/path_storage_viewer.dart
  71. 317 0
      lib/ui/viewer/actions/file_selection_actions_widget.dart
  72. 16 0
      lib/ui/viewer/actions/file_selection_common_actions_widget.dart
  73. 167 0
      lib/ui/viewer/actions/file_selection_overlay_bar.dart
  74. 10 5
      lib/ui/viewer/file/collections_list_of_file_widget.dart
  75. 1 1
      lib/ui/viewer/file/fading_bottom_bar.dart
  76. 41 2
      lib/ui/viewer/file/file_caption_widget.dart
  77. 180 64
      lib/ui/viewer/file/file_icons_widget.dart
  78. 49 8
      lib/ui/viewer/file/file_info_widget.dart
  79. 30 0
      lib/ui/viewer/file/thumbnail_widget.dart
  80. 3 3
      lib/ui/viewer/gallery/archive_page.dart
  81. 49 17
      lib/ui/viewer/gallery/collection_page.dart
  82. 2 2
      lib/ui/viewer/gallery/device_folder_page.dart
  83. 61 58
      lib/ui/viewer/gallery/empty_hidden_widget.dart
  84. 23 20
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  85. 3 3
      lib/ui/viewer/gallery/hidden_page.dart
  86. 3 3
      lib/ui/viewer/search/result/search_result_page.dart
  87. 99 0
      lib/utils/delete_file_util.dart
  88. 6 2
      lib/utils/dialog_util.dart
  89. 3 2
      lib/utils/email_util.dart
  90. 13 0
      lib/utils/separators_util.dart
  91. 18 1
      lib/utils/share_util.dart
  92. 1 1
      pubspec.yaml
  93. 1 0
      test/utils/date_time_util_test.dart

+ 10 - 5
README.md

@@ -4,7 +4,7 @@
 
 
 We have open-source apps across Android, iOS, web and desktop that automatically backup your photos and videos.
 We have open-source apps across Android, iOS, web and desktop that automatically backup your photos and videos.
 
 
-This repository contains the code for our mobile apps, built with a lot of ❤️, and a little bit of [Flutter.](https://flutter.dev)
+This repository contains the code for our mobile apps, built with a lot of ❤️, and a little bit of [Flutter](https://flutter.dev).
 
 
 ![App Screenshots](https://user-images.githubusercontent.com/24503581/175218240-fe5a0703-82c1-4750-bfea-abfd9f409a97.png)
 ![App Screenshots](https://user-images.githubusercontent.com/24503581/175218240-fe5a0703-82c1-4750-bfea-abfd9f409a97.png)
 
 
@@ -35,7 +35,11 @@ This repository contains the code for our mobile apps, built with a lot of ❤
 
 
 ### Android
 ### Android
 
 
-This [repository's GitHub releases](https://github.com/ente-io/frame/releases) contains APKs, built straight from source. The latest build is available @ [ente.io/apk](https://ente.io/apk). These builds keep themselves updated, without relying on third party stores.
+This [repository's GitHub
+releases](https://github.com/ente-io/photos-app/releases) contains APKs, built
+straight from source. The latest build is available @
+[ente.io/apk](https://ente.io/apk). These builds keep themselves updated,
+without relying on third party stores.
 
 
 You can alternatively install the build from PlayStore or F-Droid.
 You can alternatively install the build from PlayStore or F-Droid.
 
 
@@ -58,10 +62,10 @@ You can alternatively install the build from PlayStore or F-Droid.
 ## 🧑‍💻 Building from source
 ## 🧑‍💻 Building from source
 
 
 1. [Install Flutter](https://flutter.dev/docs/get-started/install)
 1. [Install Flutter](https://flutter.dev/docs/get-started/install)
-2. Clone this repository with `git clone git@github.com:ente-io/frame.git` 
+2. Clone this repository with `git clone git@github.com:ente-io/photos-app.git`
 3. Pull in all submodules with `git submodule update --init --recursive`
 3. Pull in all submodules with `git submodule update --init --recursive`
 4. For Android, run `flutter build apk --release --flavor independent`
 4. For Android, run `flutter build apk --release --flavor independent`
-5. For iOS, run `flutter build ios` 
+5. For iOS, run `flutter build ios`
 
 
 <br/>
 <br/>
 
 
@@ -81,7 +85,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
 
 
 If you like this project, please consider upgrading to a paid subscription.
 If you like this project, please consider upgrading to a paid subscription.
 
 
-If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/frame/stargazers) this project.
+If you would like to motivate us to keep building, you can do so by
+[starring](https://github.com/ente-io/photos-app/stargazers) this project.
 
 
 <br/>
 <br/>
 
 

+ 1 - 1
fastlane/metadata/android/en-US/full_description.txt

@@ -24,7 +24,7 @@ FEATURES
 - and a LOT more!
 - and a LOT more!
 
 
 PERMISSIONS
 PERMISSIONS
-Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/frame/blob/f-droid/android/permissions.md
+Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
 
 
 PRICING
 PRICING
 We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
 We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.

+ 7 - 0
lib/core/constants.dart

@@ -18,6 +18,8 @@ const int batchSize = 1000;
 const photoGridSizeDefault = 4;
 const photoGridSizeDefault = 4;
 const photoGridSizeMin = 2;
 const photoGridSizeMin = 2;
 const photoGridSizeMax = 6;
 const photoGridSizeMax = 6;
+const subGalleryLimitDefault = 80;
+const subGalleryLimitMin = 40;
 
 
 // used to identify which ente file are available in app cache
 // used to identify which ente file are available in app cache
 // todo: 6Jun22: delete old media identifier after 3 months
 // todo: 6Jun22: delete old media identifier after 3 months
@@ -49,3 +51,8 @@ class FFDefault {
 const kDefaultProductionEndpoint = 'https://api.ente.io';
 const kDefaultProductionEndpoint = 'https://api.ente.io';
 
 
 const int intMaxValue = 9223372036854775807;
 const int intMaxValue = 9223372036854775807;
+
+//Screen width of iPhone 14 pro max in points is taken as maximum
+const double restrictedMaxWidth = 430;
+
+const double mobileSmallThreshold = 336;

+ 0 - 12
lib/db/files_db.dart

@@ -817,18 +817,6 @@ class FilesDB {
     );
     );
   }
   }
 
 
-  Future<int> getNumberOfUploadedFiles() async {
-    final db = await instance.database;
-    final rows = await db.query(
-      filesTable,
-      columns: [columnUploadedFileID],
-      where:
-          '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NOT NULL)',
-      distinct: true,
-    );
-    return rows.length;
-  }
-
   Future<int> updateUploadedFile(
   Future<int> updateUploadedFile(
     String localID,
     String localID,
     String title,
     String title,

+ 3 - 0
lib/ente_theme_data.dart

@@ -343,6 +343,9 @@ extension CustomColorScheme on ColorScheme {
 
 
   EnteTheme get enteTheme =>
   EnteTheme get enteTheme =>
       brightness == Brightness.light ? lightTheme : darkTheme;
       brightness == Brightness.light ? lightTheme : darkTheme;
+
+  EnteTheme get inverseEnteTheme =>
+      brightness == Brightness.light ? darkTheme : lightTheme;
 }
 }
 
 
 OutlinedButtonThemeData buildOutlinedButtonThemeData({
 OutlinedButtonThemeData buildOutlinedButtonThemeData({

+ 12 - 0
lib/extensions/string_ext.dart

@@ -0,0 +1,12 @@
+extension StringExtensionsNullSafe on String? {
+  int get sumAsciiValues {
+    if (this == null) {
+      return -1;
+    }
+    int sum = 0;
+    for (int i = 0; i < this!.length; i++) {
+      sum += this!.codeUnitAt(i);
+    }
+    return sum;
+  }
+}

+ 3 - 3
lib/models/api/collection/create_request.dart

@@ -6,7 +6,7 @@ class CreateRequest {
   String keyDecryptionNonce;
   String keyDecryptionNonce;
   String encryptedName;
   String encryptedName;
   String nameDecryptionNonce;
   String nameDecryptionNonce;
-  String type;
+  CollectionType type;
   CollectionAttributes? attributes;
   CollectionAttributes? attributes;
   MetadataRequest? magicMetadata;
   MetadataRequest? magicMetadata;
 
 
@@ -25,7 +25,7 @@ class CreateRequest {
     String? keyDecryptionNonce,
     String? keyDecryptionNonce,
     String? encryptedName,
     String? encryptedName,
     String? nameDecryptionNonce,
     String? nameDecryptionNonce,
-    String? type,
+    CollectionType? type,
     CollectionAttributes? attributes,
     CollectionAttributes? attributes,
     MetadataRequest? magicMetadata,
     MetadataRequest? magicMetadata,
   }) =>
   }) =>
@@ -45,7 +45,7 @@ class CreateRequest {
     map['keyDecryptionNonce'] = keyDecryptionNonce;
     map['keyDecryptionNonce'] = keyDecryptionNonce;
     map['encryptedName'] = encryptedName;
     map['encryptedName'] = encryptedName;
     map['nameDecryptionNonce'] = nameDecryptionNonce;
     map['nameDecryptionNonce'] = nameDecryptionNonce;
-    map['type'] = type;
+    map['type'] = Collection.typeToString(type);
     if (attributes != null) {
     if (attributes != null) {
       map['attributes'] = attributes!.toMap();
       map['attributes'] = attributes!.toMap();
     }
     }

+ 72 - 7
lib/models/collection.dart

@@ -1,6 +1,7 @@
 import 'dart:convert';
 import 'dart:convert';
 import 'dart:core';
 import 'dart:core';
 
 
+import 'package:flutter/foundation.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/magic_metadata.dart';
 
 
 class Collection {
 class Collection {
@@ -57,14 +58,43 @@ class Collection {
     return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
     return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
   }
   }
 
 
+  List<User> getSharees() {
+    final List<User> result = [];
+    if (sharees == null) {
+      return result;
+    }
+    for (final User? u in sharees!) {
+      if (u != null) {
+        result.add(u);
+      }
+    }
+    return result;
+  }
+
+  bool isOwner(int userID) {
+    return (owner?.id ?? 0) == userID;
+  }
+
+  void updateSharees(List<User> newSharees) {
+    sharees?.clear();
+    sharees?.addAll(newSharees);
+  }
+
   static CollectionType typeFromString(String type) {
   static CollectionType typeFromString(String type) {
     switch (type) {
     switch (type) {
       case "folder":
       case "folder":
         return CollectionType.folder;
         return CollectionType.folder;
       case "favorites":
       case "favorites":
         return CollectionType.favorites;
         return CollectionType.favorites;
+      case "uncategorized":
+        return CollectionType.uncategorized;
+      case "album":
+        return CollectionType.album;
+      case "unknown":
+        return CollectionType.unknown;
     }
     }
-    return CollectionType.album;
+    debugPrint("unexpected collection type $type");
+    return CollectionType.unknown;
   }
   }
 
 
   static String typeToString(CollectionType type) {
   static String typeToString(CollectionType type) {
@@ -73,8 +103,12 @@ class Collection {
         return "folder";
         return "folder";
       case CollectionType.favorites:
       case CollectionType.favorites:
         return "favorites";
         return "favorites";
-      default:
+      case CollectionType.album:
         return "album";
         return "album";
+      case CollectionType.uncategorized:
+        return "uncategorized";
+      case CollectionType.unknown:
+        return "unknown";
     }
     }
   }
   }
 
 
@@ -165,7 +199,34 @@ class Collection {
 enum CollectionType {
 enum CollectionType {
   folder,
   folder,
   favorites,
   favorites,
+  uncategorized,
   album,
   album,
+  unknown,
+}
+
+enum CollectionParticipantRole {
+  unknown,
+  viewer,
+  collaborator,
+  owner,
+}
+
+extension CollectionParticipantRoleExtn on CollectionParticipantRole {
+  static CollectionParticipantRole fromString(String? val) {
+    if ((val ?? '') == '') {
+      return CollectionParticipantRole.viewer;
+    }
+    for (var x in CollectionParticipantRole.values) {
+      if (x.name.toUpperCase() == val!.toUpperCase()) {
+        return x;
+      }
+    }
+    return CollectionParticipantRole.unknown;
+  }
+
+  String toStringVal() {
+    return name.toUpperCase();
+  }
 }
 }
 
 
 class CollectionAttributes {
 class CollectionAttributes {
@@ -206,19 +267,22 @@ class User {
   int? id;
   int? id;
   String email;
   String email;
   String? name;
   String? name;
+  String? role;
 
 
   User({
   User({
     this.id,
     this.id,
     required this.email,
     required this.email,
     this.name,
     this.name,
+    this.role,
   });
   });
 
 
+  bool get isViewer => role == null || role?.toUpperCase() == 'VIEWER';
+
+  bool get isCollaborator =>
+      role != null && role?.toUpperCase() == 'COLLABORATOR';
+
   Map<String, dynamic> toMap() {
   Map<String, dynamic> toMap() {
-    return {
-      'id': id,
-      'email': email,
-      'name': name,
-    };
+    return {'id': id, 'email': email, 'name': name, 'role': role};
   }
   }
 
 
   static fromMap(Map<String, dynamic>? map) {
   static fromMap(Map<String, dynamic>? map) {
@@ -228,6 +292,7 @@ class User {
       id: map['id'],
       id: map['id'],
       email: map['email'],
       email: map['email'],
       name: map['name'],
       name: map['name'],
+      role: map['role'] ?? 'VIEWER',
     );
     );
   }
   }
 
 

+ 150 - 0
lib/models/gallery_type.dart

@@ -2,6 +2,7 @@ enum GalleryType {
   homepage,
   homepage,
   archive,
   archive,
   hidden,
   hidden,
+  favorite,
   trash,
   trash,
   localFolder,
   localFolder,
   // indicator for gallery view of collections shared with the user
   // indicator for gallery view of collections shared with the user
@@ -9,3 +10,152 @@ enum GalleryType {
   ownedCollection,
   ownedCollection,
   searchResults
   searchResults
 }
 }
+
+extension GalleyTypeExtension on GalleryType {
+  bool showAddToAlbum() {
+    switch (this) {
+      case GalleryType.homepage:
+      case GalleryType.archive:
+      case GalleryType.localFolder:
+      case GalleryType.ownedCollection:
+      case GalleryType.searchResults:
+      case GalleryType.favorite:
+        return true;
+
+      case GalleryType.hidden:
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showMoveToAlbum() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+        return true;
+
+      case GalleryType.hidden:
+      case GalleryType.favorite:
+      case GalleryType.searchResults:
+      case GalleryType.archive:
+      case GalleryType.localFolder:
+      case GalleryType.homepage:
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  // showDeleteTopOption indicates whether we should show
+  // delete icon as iconButton
+  bool showDeleteIconOption() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.searchResults:
+      case GalleryType.homepage:
+      case GalleryType.favorite:
+      case GalleryType.localFolder:
+        return true;
+      case GalleryType.trash:
+      case GalleryType.archive:
+      case GalleryType.hidden:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showDeleteOption() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.searchResults:
+      case GalleryType.homepage:
+      case GalleryType.favorite:
+      case GalleryType.archive:
+      case GalleryType.hidden:
+      case GalleryType.localFolder:
+        return true;
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showRemoveFromAlbum() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.sharedCollection:
+        return true;
+      case GalleryType.hidden:
+      case GalleryType.favorite:
+      case GalleryType.searchResults:
+      case GalleryType.homepage:
+      case GalleryType.archive:
+      case GalleryType.localFolder:
+      case GalleryType.trash:
+        return false;
+    }
+  }
+
+  bool showArchiveOption() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.homepage:
+        return true;
+
+      case GalleryType.hidden:
+      case GalleryType.favorite:
+      case GalleryType.searchResults:
+      case GalleryType.archive:
+      case GalleryType.localFolder:
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showUnArchiveOption() {
+    return this == GalleryType.archive;
+  }
+
+  bool showHideOption() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.homepage:
+      case GalleryType.searchResults:
+      case GalleryType.archive:
+        return true;
+
+      case GalleryType.hidden:
+      case GalleryType.localFolder:
+      case GalleryType.trash:
+      case GalleryType.favorite:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showUnHideOption() {
+    return this == GalleryType.hidden;
+  }
+
+  bool showFavoriteOption() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.homepage:
+      case GalleryType.searchResults:
+        return true;
+
+      case GalleryType.hidden:
+      case GalleryType.favorite:
+      case GalleryType.archive:
+      case GalleryType.localFolder:
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
+  bool showUnFavoriteOption() {
+    return this == GalleryType.favorite;
+  }
+}

+ 9 - 1
lib/models/magic_metadata.dart

@@ -15,6 +15,7 @@ const subTypeKey = 'subType';
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedName = 'editedName';
 const pubMagicKeyEditedName = 'editedName';
 const pubMagicKeyCaption = "caption";
 const pubMagicKeyCaption = "caption";
+const pubMagicKeyUploaderName = "uploaderName";
 
 
 class MagicMetadata {
 class MagicMetadata {
   // 0 -> visible
   // 0 -> visible
@@ -41,8 +42,14 @@ class PubMagicMetadata {
   int? editedTime;
   int? editedTime;
   String? editedName;
   String? editedName;
   String? caption;
   String? caption;
+  String? uploaderName;
 
 
-  PubMagicMetadata({this.editedTime, this.editedName, this.caption});
+  PubMagicMetadata({
+    this.editedTime,
+    this.editedName,
+    this.caption,
+    this.uploaderName,
+  });
 
 
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -56,6 +63,7 @@ class PubMagicMetadata {
       editedTime: map[pubMagicKeyEditedTime],
       editedTime: map[pubMagicKeyEditedTime],
       editedName: map[pubMagicKeyEditedName],
       editedName: map[pubMagicKeyEditedName],
       caption: map[pubMagicKeyCaption],
       caption: map[pubMagicKeyCaption],
+      uploaderName: map[pubMagicKeyUploaderName],
     );
     );
   }
   }
 }
 }

+ 16 - 0
lib/models/selected_file_breakup.dart

@@ -0,0 +1,16 @@
+import 'package:photos/models/file.dart';
+
+class SelectedFileSplit {
+  final List<File> pendingUploads;
+  final List<File> ownedByCurrentUser;
+  final List<File> ownedByOtherUsers;
+
+  SelectedFileSplit({
+    required this.pendingUploads,
+    required this.ownedByCurrentUser,
+    required this.ownedByOtherUsers,
+  });
+
+  int get totalFileOwnedCount =>
+      pendingUploads.length + ownedByCurrentUser.length;
+}

+ 39 - 5
lib/models/selected_files.dart

@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/clear_selections_event.dart';
 import 'package:photos/events/clear_selections_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/models/selected_file_breakup.dart';
 
 
 class SelectedFiles extends ChangeNotifier {
 class SelectedFiles extends ChangeNotifier {
   final files = <File>{};
   final files = <File>{};
@@ -13,7 +14,7 @@ class SelectedFiles extends ChangeNotifier {
     // or any other update, using file.generatedID to track if this file was already
     // or any other update, using file.generatedID to track if this file was already
     // selected or not
     // selected or not
     final File? alreadySelected = files.firstWhereOrNull(
     final File? alreadySelected = files.firstWhereOrNull(
-      (element) => element.generatedID == file.generatedID,
+      (element) => _isMatch(file, element),
     );
     );
     if (alreadySelected != null) {
     if (alreadySelected != null) {
       files.remove(alreadySelected);
       files.remove(alreadySelected);
@@ -32,26 +33,59 @@ class SelectedFiles extends ChangeNotifier {
     notifyListeners();
     notifyListeners();
   }
   }
 
 
-  void unSelectAll(Set<File> selectedFiles) {
+  void unSelectAll(Set<File> selectedFiles, {bool skipNotify = false}) {
     files.removeAll(selectedFiles);
     files.removeAll(selectedFiles);
     lastSelections.clear();
     lastSelections.clear();
-    notifyListeners();
+    if (!skipNotify) {
+      notifyListeners();
+    }
   }
   }
 
 
   bool isFileSelected(File file) {
   bool isFileSelected(File file) {
     final File? alreadySelected = files.firstWhereOrNull(
     final File? alreadySelected = files.firstWhereOrNull(
-      (element) => element.generatedID == file.generatedID,
+      (element) => _isMatch(file, element),
     );
     );
     return alreadySelected != null;
     return alreadySelected != null;
   }
   }
 
 
   bool isPartOfLastSelected(File file) {
   bool isPartOfLastSelected(File file) {
     final File? matchedFile = lastSelections.firstWhereOrNull(
     final File? matchedFile = lastSelections.firstWhereOrNull(
-      (element) => element.generatedID == file.generatedID,
+      (element) => _isMatch(file, element),
     );
     );
     return matchedFile != null;
     return matchedFile != null;
   }
   }
 
 
+  bool _isMatch(File first, File second) {
+    if (first.generatedID != null && second.generatedID != null) {
+      if (first.generatedID == second.generatedID) {
+        return true;
+      }
+    } else if (first.uploadedFileID != null && second.uploadedFileID != null) {
+      return first.uploadedFileID == second.uploadedFileID;
+    }
+    return false;
+  }
+
+  SelectedFileSplit split(int currentUseID) {
+    final List<File> ownedByCurrentUser = [],
+        ownedByOtherUsers = [],
+        pendingUploads = [];
+    for (var f in files) {
+      if (f.ownerID == null || f.uploadedFileID == null) {
+        pendingUploads.add(f);
+      } else if (f.ownerID == currentUseID) {
+        ownedByCurrentUser.add(f);
+      } else {
+        ownedByOtherUsers.add(f);
+      }
+    }
+    return SelectedFileSplit(
+      pendingUploads: pendingUploads,
+      ownedByCurrentUser: ownedByCurrentUser,
+      ownedByOtherUsers: ownedByOtherUsers,
+    );
+  }
+
   void clearAll() {
   void clearAll() {
     Bus.instance.fire(ClearSelectionsEvent());
     Bus.instance.fire(ClearSelectionsEvent());
     lastSelections.addAll(files);
     lastSelections.addAll(files);

+ 1 - 12
lib/models/user_details.dart

@@ -1,11 +1,10 @@
 import 'dart:math';
 import 'dart:math';
 
 
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
 import 'package:photos/models/file_type.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 {
   final String email;
   final String email;
   final int usage;
   final int usage;
   final int fileCount;
   final int fileCount;
@@ -22,16 +21,6 @@ class UserDetails extends Equatable {
     this.familyData,
     this.familyData,
   );
   );
 
 
-  @override
-  List<Object?> get props => [
-        email,
-        usage,
-        fileCount,
-        sharedCollectionsCount,
-        subscription,
-        familyData
-      ];
-
   bool isPartOfFamily() {
   bool isPartOfFamily() {
     return familyData?.members?.isNotEmpty ?? false;
     return familyData?.members?.isNotEmpty ?? false;
   }
   }

+ 67 - 9
lib/services/collections_service.dart

@@ -5,6 +5,7 @@ import 'dart:convert';
 import 'dart:math';
 import 'dart:math';
 import 'dart:typed_data';
 import 'dart:typed_data';
 
 
+import 'package:collection/collection.dart';
 import 'package:dio/dio.dart';
 import 'package:dio/dio.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
@@ -55,6 +56,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>{};
+  final _cachedUserIdToUser = <int, User>{};
   Collection cachedDefaultHiddenCollection;
   Collection cachedDefaultHiddenCollection;
 
 
   CollectionsService._privateConstructor() {
   CollectionsService._privateConstructor() {
@@ -219,8 +221,37 @@ class CollectionsService {
         .toList();
         .toList();
   }
   }
 
 
+  User getFileOwner(int userID, int collectionID) {
+    if (_cachedUserIdToUser.containsKey(userID)) {
+      return _cachedUserIdToUser[userID];
+    }
+    if (collectionID != null) {
+      final Collection collection = getCollectionByID(collectionID);
+      if (collection != null) {
+        if (collection.owner.id == userID) {
+          _cachedUserIdToUser[userID] = collection.owner;
+        } else {
+          final matchingUser = collection.getSharees().firstWhereOrNull(
+                (u) => u.id == userID,
+              );
+          if (matchingUser != null) {
+            _cachedUserIdToUser[userID] = collection.owner;
+          }
+        }
+      }
+    }
+    return _cachedUserIdToUser[userID] ??
+        User(
+          id: userID,
+          email: "unknown@unknown.com",
+        );
+  }
+
   Future<List<CollectionWithThumbnail>> getCollectionsWithThumbnails({
   Future<List<CollectionWithThumbnail>> getCollectionsWithThumbnails({
     bool includedOwnedByOthers = false,
     bool includedOwnedByOthers = false,
+    // includeCollabCollections will include collections where the current user
+    // is added as a collaborator
+    bool includeCollabCollections = false,
   }) async {
   }) async {
     final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
     final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
     final usersCollection = getActiveCollections();
     final usersCollection = getActiveCollections();
@@ -228,7 +259,15 @@ class CollectionsService {
     usersCollection.removeWhere((element) => element.isHidden());
     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);
+      if (includeCollabCollections) {
+        usersCollection.removeWhere(
+          (c) =>
+              (c.owner.id != userID) &&
+              (c.getSharees().any((u) => (u.id ?? -1) == userID && u.isViewer)),
+        );
+      } else {
+        usersCollection.removeWhere((c) => c.owner.id != userID);
+      }
     }
     }
     final latestCollectionFiles = await getLatestCollectionFiles();
     final latestCollectionFiles = await getLatestCollectionFiles();
     final Map<int, File> collectionToThumbnailMap = Map.fromEntries(
     final Map<int, File> collectionToThumbnailMap = Map.fromEntries(
@@ -258,42 +297,61 @@ class CollectionsService {
     });
     });
   }
   }
 
 
-  Future<void> share(int collectionID, String email, String publicKey) async {
+  Future<List<User>> share(
+    int collectionID,
+    String email,
+    String publicKey,
+    CollectionParticipantRole role,
+  ) async {
     final encryptedKey = CryptoUtil.sealSync(
     final encryptedKey = CryptoUtil.sealSync(
       getCollectionKey(collectionID),
       getCollectionKey(collectionID),
       Sodium.base642bin(publicKey),
       Sodium.base642bin(publicKey),
     );
     );
     try {
     try {
-      await _enteDio.post(
+      final response = await _enteDio.post(
         "/collections/share",
         "/collections/share",
         data: {
         data: {
           "collectionID": collectionID,
           "collectionID": collectionID,
           "email": email,
           "email": email,
           "encryptedKey": Sodium.bin2base64(encryptedKey),
           "encryptedKey": Sodium.bin2base64(encryptedKey),
+          "role": role.toStringVal()
         },
         },
       );
       );
+      final sharees = <User>[];
+      for (final user in response.data["sharees"]) {
+        sharees.add(User.fromMap(user));
+      }
+      _collectionIDToCollections[collectionID] =
+          _collectionIDToCollections[collectionID].copyWith(sharees: sharees);
+      unawaited(_db.insert([_collectionIDToCollections[collectionID]]));
+      RemoteSyncService.instance.sync(silently: true).ignore();
+      return sharees;
     } on DioError catch (e) {
     } on DioError catch (e) {
       if (e.response.statusCode == 402) {
       if (e.response.statusCode == 402) {
         throw SharingNotPermittedForFreeAccountsError();
         throw SharingNotPermittedForFreeAccountsError();
       }
       }
       rethrow;
       rethrow;
     }
     }
-    RemoteSyncService.instance.sync(silently: true);
   }
   }
 
 
-  Future<void> unshare(int collectionID, String email) async {
+  Future<List<User>> unshare(int collectionID, String email) async {
     try {
     try {
-      await _enteDio.post(
+      final response = await _enteDio.post(
         "/collections/unshare",
         "/collections/unshare",
         data: {
         data: {
           "collectionID": collectionID,
           "collectionID": collectionID,
           "email": email,
           "email": email,
         },
         },
       );
       );
-      _collectionIDToCollections[collectionID]
-          .sharees
-          .removeWhere((user) => user.email == email);
+      final sharees = <User>[];
+      for (final user in response.data["sharees"]) {
+        sharees.add(User.fromMap(user));
+      }
+      _collectionIDToCollections[collectionID] =
+          _collectionIDToCollections[collectionID].copyWith(sharees: sharees);
       unawaited(_db.insert([_collectionIDToCollections[collectionID]]));
       unawaited(_db.insert([_collectionIDToCollections[collectionID]]));
+      RemoteSyncService.instance.sync(silently: true).ignore();
+      return sharees;
     } catch (e) {
     } catch (e) {
       _logger.severe(e);
       _logger.severe(e);
       rethrow;
       rethrow;

+ 23 - 2
lib/services/favorites_service.dart

@@ -35,7 +35,10 @@ class FavoritesService {
           _cachedFavoritesCollectionID != null &&
           _cachedFavoritesCollectionID != null &&
           _cachedFavoritesCollectionID == event.collectionID) {
           _cachedFavoritesCollectionID == event.collectionID) {
         if (event.type == EventType.addedOrUpdated) {
         if (event.type == EventType.addedOrUpdated) {
-          _updateFavoriteFilesCache(event.updatedFiles, favFlag: true);
+          // Note: This source check is a ugly hack because currently we
+          // don't have any event type related to remove from collection
+          final bool isAdded = !event.source.contains("remove");
+          _updateFavoriteFilesCache(event.updatedFiles, favFlag: isAdded);
         } else if (event.type == EventType.deletedFromEverywhere ||
         } else if (event.type == EventType.deletedFromEverywhere ||
             event.type == EventType.deletedFromRemote) {
             event.type == EventType.deletedFromRemote) {
           _updateFavoriteFilesCache(event.updatedFiles, favFlag: false);
           _updateFavoriteFilesCache(event.updatedFiles, favFlag: false);
@@ -70,9 +73,10 @@ class FavoritesService {
     if (file.collectionID != null &&
     if (file.collectionID != null &&
         _cachedFavoritesCollectionID != null &&
         _cachedFavoritesCollectionID != null &&
         file.collectionID == _cachedFavoritesCollectionID) {
         file.collectionID == _cachedFavoritesCollectionID) {
+      debugPrint("File ${file.uploadedFileID} is part of favorite collection");
       return true;
       return true;
     }
     }
-    if(checkOnlyAlbum) {
+    if (checkOnlyAlbum) {
       return false;
       return false;
     }
     }
     if (file.uploadedFileID != null) {
     if (file.uploadedFileID != null) {
@@ -129,6 +133,23 @@ class FavoritesService {
     RemoteSyncService.instance.sync(silently: true);
     RemoteSyncService.instance.sync(silently: true);
   }
   }
 
 
+  Future<void> updateFavorites(List<File> files, bool favFlag) async {
+    final int currentUserID = Configuration.instance.getUserID();
+    if (files.any((f) => f.uploadedFileID == null)) {
+      throw AssertionError("Can only favorite uploaded items");
+    }
+    if (files.any((f) => f.ownerID != currentUserID)) {
+      throw AssertionError("Can not favortie files owned by others");
+    }
+    final collectionID = await _getOrCreateFavoriteCollectionID();
+    if (favFlag) {
+      await _collectionsService.addToCollection(collectionID, files);
+    } else {
+      await _collectionsService.removeFromCollection(collectionID, files);
+    }
+    _updateFavoriteFilesCache(files, favFlag: favFlag);
+  }
+
   Future<void> removeFromFavorites(File file) async {
   Future<void> removeFromFavorites(File file) async {
     final collectionID = await _getOrCreateFavoriteCollectionID();
     final collectionID = await _getOrCreateFavoriteCollectionID();
     final fileID = file.uploadedFileID;
     final fileID = file.uploadedFileID;

+ 2 - 1
lib/services/hidden_service.dart

@@ -86,6 +86,7 @@ extension HiddenService on CollectionsService {
     } on AssertionError catch (e) {
     } on AssertionError catch (e) {
       await dialog.hide();
       await dialog.hide();
       showErrorDialog(context, "Oops", e.message as String);
       showErrorDialog(context, "Oops", e.message as String);
+      return false;
     } catch (e, s) {
     } catch (e, s) {
       _logger.severe("Could not hide", e, s);
       _logger.severe("Could not hide", e, s);
       await dialog.hide();
       await dialog.hide();
@@ -124,7 +125,7 @@ extension HiddenService on CollectionsService {
       keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
       keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
       encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
       encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
       nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
       nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
-      type: CollectionType.album.toString(),
+      type: CollectionType.album,
       attributes: CollectionAttributes(),
       attributes: CollectionAttributes(),
       magicMetadata: metadataRequest,
       magicMetadata: metadataRequest,
     );
     );

+ 1 - 1
lib/services/remote_sync_service.dart

@@ -502,7 +502,7 @@ class RemoteSyncService {
         break;
         break;
       }
       }
       // prefer existing collection ID for manually uploaded files.
       // prefer existing collection ID for manually uploaded files.
-      // See https://github.com/ente-io/frame/pull/187
+      // See https://github.com/ente-io/photos-app/pull/187
       final collectionID = file.collectionID ??
       final collectionID = file.collectionID ??
           (await _collectionsService.getOrCreateForPath(file.deviceFolder)).id;
           (await _collectionsService.getOrCreateForPath(file.deviceFolder)).id;
       _uploadFile(file, collectionID, futures);
       _uploadFile(file, collectionID, futures);

+ 1 - 1
lib/services/update_service.dart

@@ -17,7 +17,7 @@ class UpdateService {
   static final UpdateService instance = UpdateService._privateConstructor();
   static final UpdateService instance = UpdateService._privateConstructor();
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
   static const changeLogVersionKey = "update_change_log_key";
   static const changeLogVersionKey = "update_change_log_key";
-  static const currentChangeLogVersion = 2;
+  static const currentChangeLogVersion = 3;
 
 
   LatestVersionInfo _latestVersion;
   LatestVersionInfo _latestVersion;
   final _logger = Logger("UpdateService");
   final _logger = Logger("UpdateService");

+ 1 - 1
lib/services/user_service.dart

@@ -136,7 +136,7 @@ class UserService {
   Future<UserDetails> getUserDetailsV2({bool memoryCount = true}) async {
   Future<UserDetails> getUserDetailsV2({bool memoryCount = true}) async {
     try {
     try {
       final response = await _enteDio.get(
       final response = await _enteDio.get(
-        "/users/details/v2?memoryCount=$memoryCount",
+        "/users/details/v2",
         queryParameters: {
         queryParameters: {
           "memoryCount": memoryCount,
           "memoryCount": memoryCount,
         },
         },

+ 11 - 20
lib/states/user_details_state.dart

@@ -1,10 +1,8 @@
 import 'dart:async';
 import 'dart:async';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:photos/core/configuration.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/events/user_details_changed_event.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/models/user_details.dart';
 // ignore: import_of_legacy_library_into_null_safe
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
@@ -21,26 +19,17 @@ class UserDetailsStateWidget extends StatefulWidget {
 }
 }
 
 
 class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
 class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
-  late Future<UserDetails?> userDetails;
-  late StreamSubscription<UserDetailsChangedEvent> _userDetailsChangedEvent;
+  late UserDetails? userDetails;
   late StreamSubscription<OpenedSettingsEvent> _openedSettingsEventSubscription;
   late StreamSubscription<OpenedSettingsEvent> _openedSettingsEventSubscription;
 
 
   @override
   @override
   void initState() {
   void initState() {
-    if (Configuration.instance.hasConfiguredAccount()) {
-      _fetchUserDetails();
-    } else {
-      userDetails = Future.value(null);
-    }
-    _userDetailsChangedEvent =
-        Bus.instance.on<UserDetailsChangedEvent>().listen((event) {
-      _fetchUserDetails();
-    });
+    userDetails = null;
     _openedSettingsEventSubscription =
     _openedSettingsEventSubscription =
         Bus.instance.on<OpenedSettingsEvent>().listen((event) {
         Bus.instance.on<OpenedSettingsEvent>().listen((event) {
       Future.delayed(
       Future.delayed(
         const Duration(
         const Duration(
-          seconds: 1,
+          milliseconds: 750,
         ),
         ),
         _fetchUserDetails,
         _fetchUserDetails,
       );
       );
@@ -50,7 +39,6 @@ class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
 
 
   @override
   @override
   void dispose() {
   void dispose() {
-    _userDetailsChangedEvent.cancel();
     _openedSettingsEventSubscription.cancel();
     _openedSettingsEventSubscription.cancel();
     super.dispose();
     super.dispose();
   }
   }
@@ -62,8 +50,9 @@ class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
         child: widget.child,
         child: widget.child,
       );
       );
 
 
-  void _fetchUserDetails() {
-    userDetails = UserService.instance.getUserDetailsV2(memoryCount: true);
+  void _fetchUserDetails() async {
+    userDetails =
+        await UserService.instance.getUserDetailsV2(memoryCount: true);
     if (mounted) {
     if (mounted) {
       setState(() {});
       setState(() {});
     }
     }
@@ -72,7 +61,7 @@ class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
 
 
 class InheritedUserDetails extends InheritedWidget {
 class InheritedUserDetails extends InheritedWidget {
   final UserDetailsStateWidgetState userDetailsState;
   final UserDetailsStateWidgetState userDetailsState;
-  final Future<UserDetails?> userDetails;
+  final UserDetails? userDetails;
 
 
   const InheritedUserDetails({
   const InheritedUserDetails({
     Key? key,
     Key? key,
@@ -85,6 +74,8 @@ class InheritedUserDetails extends InheritedWidget {
       context.dependOnInheritedWidgetOfExactType<InheritedUserDetails>();
       context.dependOnInheritedWidgetOfExactType<InheritedUserDetails>();
 
 
   @override
   @override
-  bool updateShouldNotify(covariant InheritedUserDetails oldWidget) =>
-      userDetails != oldWidget.userDetails;
+  bool updateShouldNotify(covariant InheritedUserDetails oldWidget) {
+    return (userDetails?.usage != oldWidget.userDetails?.usage) ||
+        (userDetails?.fileCount != oldWidget.userDetails?.fileCount);
+  }
 }
 }

+ 72 - 10
lib/theme/colors.dart

@@ -10,7 +10,7 @@ class EnteColorScheme {
 
 
   // Backdrop Colors
   // Backdrop Colors
   final Color backdropBase;
   final Color backdropBase;
-  final Color backdropBaseMute;
+  final Color backdropMuted;
   final Color backdropFaint;
   final Color backdropFaint;
 
 
   // Text Colors
   // Text Colors
@@ -23,6 +23,7 @@ class EnteColorScheme {
   final Color fillBase;
   final Color fillBase;
   final Color fillMuted;
   final Color fillMuted;
   final Color fillFaint;
   final Color fillFaint;
+  final Color fillFaintPressed;
 
 
   // Stroke Colors
   // Stroke Colors
   final Color strokeBase;
   final Color strokeBase;
@@ -47,13 +48,14 @@ class EnteColorScheme {
 
 
   //other colors
   //other colors
   final Color tabIcon;
   final Color tabIcon;
+  final List<Color> avatarColors;
 
 
   const EnteColorScheme(
   const EnteColorScheme(
     this.backgroundBase,
     this.backgroundBase,
     this.backgroundElevated,
     this.backgroundElevated,
     this.backgroundElevated2,
     this.backgroundElevated2,
     this.backdropBase,
     this.backdropBase,
-    this.backdropBaseMute,
+    this.backdropMuted,
     this.backdropFaint,
     this.backdropFaint,
     this.textBase,
     this.textBase,
     this.textMuted,
     this.textMuted,
@@ -62,6 +64,7 @@ class EnteColorScheme {
     this.fillBase,
     this.fillBase,
     this.fillMuted,
     this.fillMuted,
     this.fillFaint,
     this.fillFaint,
+    this.fillFaintPressed,
     this.strokeBase,
     this.strokeBase,
     this.strokeMuted,
     this.strokeMuted,
     this.strokeFaint,
     this.strokeFaint,
@@ -69,7 +72,8 @@ class EnteColorScheme {
     this.blurStrokeBase,
     this.blurStrokeBase,
     this.blurStrokeFaint,
     this.blurStrokeFaint,
     this.blurStrokePressed,
     this.blurStrokePressed,
-    this.tabIcon, {
+    this.tabIcon,
+    this.avatarColors, {
     this.primary700 = _primary700,
     this.primary700 = _primary700,
     this.primary500 = _primary500,
     this.primary500 = _primary500,
     this.primary400 = _primary400,
     this.primary400 = _primary400,
@@ -95,6 +99,7 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   fillBaseLight,
   fillBaseLight,
   fillMutedLight,
   fillMutedLight,
   fillFaintLight,
   fillFaintLight,
+  fillFaintPressedLight,
   strokeBaseLight,
   strokeBaseLight,
   strokeMutedLight,
   strokeMutedLight,
   strokeFaintLight,
   strokeFaintLight,
@@ -103,6 +108,7 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   blurStrokeFaintLight,
   blurStrokeFaintLight,
   blurStrokePressedLight,
   blurStrokePressedLight,
   tabIconLight,
   tabIconLight,
+  avatarLight,
 );
 );
 
 
 const EnteColorScheme darkScheme = EnteColorScheme(
 const EnteColorScheme darkScheme = EnteColorScheme(
@@ -119,6 +125,7 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   fillBaseDark,
   fillBaseDark,
   fillMutedDark,
   fillMutedDark,
   fillFaintDark,
   fillFaintDark,
+  fillFaintPressedDark,
   strokeBaseDark,
   strokeBaseDark,
   strokeMutedDark,
   strokeMutedDark,
   strokeFaintDark,
   strokeFaintDark,
@@ -127,6 +134,7 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   blurStrokeFaintDark,
   blurStrokeFaintDark,
   blurStrokePressedDark,
   blurStrokePressedDark,
   tabIconDark,
   tabIconDark,
+  avatarDark,
 );
 );
 
 
 // Background Colors
 // Background Colors
@@ -139,13 +147,13 @@ const Color backgroundElevatedDark = Color.fromRGBO(27, 27, 27, 1);
 const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
 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 backdropMutedLight = Color.fromRGBO(255, 255, 255, 0.30);
-const Color backdropFaintLight = Color.fromRGBO(255, 255, 255, 0.15);
+const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.92);
+const Color backdropMutedLight = Color.fromRGBO(255, 255, 255, 0.75);
+const Color backdropFaintLight = Color.fromRGBO(255, 255, 255, 0.30);
 
 
-const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.65);
-const Color backdropMutedDark = Color.fromRGBO(0, 0, 0, 0.20);
-const Color backdropFaintDark = Color.fromRGBO(0, 0, 0, 0.08);
+const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.90);
+const Color backdropMutedDark = Color.fromRGBO(0, 0, 0, 0.65);
+const Color backdropFaintDark = Color.fromRGBO(0, 0, 0, 0.20);
 
 
 // Text Colors
 // Text Colors
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
@@ -162,10 +170,12 @@ const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
 const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
+const Color fillFaintPressedLight = Color.fromRGBO(0, 0, 0, 0.08);
 
 
 const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
 const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
+const Color fillFaintPressedDark = Color.fromRGBO(255, 255, 255, 0.06);
 
 
 // Stroke Colors
 // Stroke Colors
 const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);
@@ -181,7 +191,7 @@ 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(255, 255, 255, 0.90);
 const Color blurStrokeBaseDark = Color.fromRGBO(255, 255, 255, 0.90);
-const Color blurStrokeFaintDark = Color.fromRGBO(255, 255, 255, 0.08);
+const Color blurStrokeFaintDark = Color.fromRGBO(255, 255, 255, 0.06);
 const Color blurStrokePressedDark = Color.fromRGBO(255, 255, 255, 0.50);
 const Color blurStrokePressedDark = Color.fromRGBO(255, 255, 255, 0.50);
 
 
 // Other colors
 // Other colors
@@ -204,3 +214,55 @@ const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
 
 
 const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);
 const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);
+
+const List<Color> avatarLight = [
+  Color.fromRGBO(118, 84, 154, 1),
+  Color.fromRGBO(223, 120, 97, 1),
+  Color.fromRGBO(148, 180, 159, 1),
+  Color.fromRGBO(135, 162, 251, 1),
+  Color.fromRGBO(198, 137, 198, 1),
+  Color.fromRGBO(198, 137, 198, 1),
+  Color.fromRGBO(50, 82, 136, 1),
+  Color.fromRGBO(133, 180, 224, 1),
+  Color.fromRGBO(193, 163, 163, 1),
+  Color.fromRGBO(193, 163, 163, 1),
+  Color.fromRGBO(66, 97, 101, 1),
+  Color.fromRGBO(66, 97, 101, 1),
+  Color.fromRGBO(66, 97, 101, 1),
+  Color.fromRGBO(221, 157, 226, 1),
+  Color.fromRGBO(130, 171, 139, 1),
+  Color.fromRGBO(155, 187, 232, 1),
+  Color.fromRGBO(143, 190, 190, 1),
+  Color.fromRGBO(138, 195, 161, 1),
+  Color.fromRGBO(168, 176, 242, 1),
+  Color.fromRGBO(176, 198, 149, 1),
+  Color.fromRGBO(233, 154, 173, 1),
+  Color.fromRGBO(209, 132, 132, 1),
+  Color.fromRGBO(120, 181, 167, 1)
+];
+
+const List<Color> avatarDark = [
+  Color.fromRGBO(118, 84, 154, 1),
+  Color.fromRGBO(223, 120, 97, 1),
+  Color.fromRGBO(148, 180, 159, 1),
+  Color.fromRGBO(135, 162, 251, 1),
+  Color.fromRGBO(198, 137, 198, 1),
+  Color.fromRGBO(147, 125, 194, 1),
+  Color.fromRGBO(50, 82, 136, 1),
+  Color.fromRGBO(133, 180, 224, 1),
+  Color.fromRGBO(193, 163, 163, 1),
+  Color.fromRGBO(225, 160, 89, 1),
+  Color.fromRGBO(66, 97, 101, 1),
+  Color.fromRGBO(107, 119, 178, 1),
+  Color.fromRGBO(149, 127, 239, 1),
+  Color.fromRGBO(221, 157, 226, 1),
+  Color.fromRGBO(130, 171, 139, 1),
+  Color.fromRGBO(155, 187, 232, 1),
+  Color.fromRGBO(143, 190, 190, 1),
+  Color.fromRGBO(138, 195, 161, 1),
+  Color.fromRGBO(168, 176, 242, 1),
+  Color.fromRGBO(176, 198, 149, 1),
+  Color.fromRGBO(233, 154, 173, 1),
+  Color.fromRGBO(209, 132, 132, 1),
+  Color.fromRGBO(120, 181, 167, 1)
+];

+ 14 - 4
lib/theme/ente_theme.dart

@@ -36,10 +36,20 @@ EnteTheme darkTheme = EnteTheme(
   shadowButton: shadowButtonDark,
   shadowButton: shadowButtonDark,
 );
 );
 
 
-EnteColorScheme getEnteColorScheme(BuildContext context) {
-  return Theme.of(context).colorScheme.enteTheme.colorScheme;
+EnteColorScheme getEnteColorScheme(
+  BuildContext context, {
+  bool inverse = false,
+}) {
+  return inverse
+      ? Theme.of(context).colorScheme.inverseEnteTheme.colorScheme
+      : Theme.of(context).colorScheme.enteTheme.colorScheme;
 }
 }
 
 
-EnteTextTheme getEnteTextTheme(BuildContext context) {
-  return Theme.of(context).colorScheme.enteTheme.textTheme;
+EnteTextTheme getEnteTextTheme(
+  BuildContext context, {
+  bool inverse = false,
+}) {
+  return inverse
+      ? Theme.of(context).colorScheme.inverseEnteTheme.textTheme
+      : Theme.of(context).colorScheme.enteTheme.textTheme;
 }
 }

+ 6 - 10
lib/ui/account/recovery_key_page.dart

@@ -172,16 +172,12 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                           ),
                           ),
                         ),
                         ),
                       ),
                       ),
-                      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,
-                          ),
+                      Padding(
+                        padding: const EdgeInsets.symmetric(vertical: 20),
+                        child: Text(
+                          widget.subText ??
+                              "We don't store this key, please save this 24 word key in a safe place.",
+                          style: Theme.of(context).textTheme.bodyText1,
                         ),
                         ),
                       ),
                       ),
                       Expanded(
                       Expanded(

+ 1 - 1
lib/ui/account/two_factor_setup_page.dart

@@ -68,6 +68,7 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
   @override
   @override
   void dispose() {
   void dispose() {
     WidgetsBinding.instance.removeObserver(_lifecycleEventHandler);
     WidgetsBinding.instance.removeObserver(_lifecycleEventHandler);
+    widget.completer.isCompleted ? null : widget.completer.complete();
     super.dispose();
     super.dispose();
   }
   }
 
 
@@ -266,7 +267,6 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
         .enableTwoFactor(context, widget.secretCode, code);
         .enableTwoFactor(context, widget.secretCode, code);
     if (success) {
     if (success) {
       _showSuccessPage();
       _showSuccessPage();
-      widget.completer.complete();
     }
     }
   }
   }
 
 

+ 134 - 0
lib/ui/actions/collection/collection_file_actions.dart

@@ -0,0 +1,134 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/widgets.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/services/favorites_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
+import 'package:photos/ui/common/progress_dialog.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/toast_util.dart';
+
+extension CollectionFileActions on CollectionActions {
+  Future<void> showRemoveFromCollectionSheet(
+    BuildContext context,
+    Collection collection,
+    SelectedFiles selectedFiles,
+  ) async {
+    final count = selectedFiles.files.length;
+    final textTheme = getEnteTextTheme(context);
+    final showDeletePrompt = await _anyItemPresentOnlyInCurrentAlbum(
+        selectedFiles.files, collection.id);
+    final String title =
+        showDeletePrompt ? "Delete items?" : "Remove from album?";
+    final String message1 = showDeletePrompt
+        ? "Some of the selected items are present only in this album and will be deleted."
+        : "Selected items will be removed from this album.";
+
+    final String message2 = showDeletePrompt
+        ? "\n\nItems which are also "
+            "present in other albums will be removed from this album but will remain elsewhere."
+        : "";
+
+    final action = CupertinoActionSheet(
+      title: Text(
+        title,
+        style: textTheme.h3Bold,
+        textAlign: TextAlign.left,
+      ),
+      message: RichText(
+        text: TextSpan(
+          children: [
+            TextSpan(text: message1, style: textTheme.body),
+            TextSpan(text: message2, style: textTheme.body)
+          ],
+        ),
+      ),
+      actions: <Widget>[
+        CupertinoActionSheetAction(
+          isDestructiveAction: true,
+          onPressed: () async {
+            Navigator.of(context, rootNavigator: true).pop();
+            final dialog = createProgressDialog(context,
+                showDeletePrompt ? "Deleting files..." : "Removing files...",);
+            await dialog.show();
+            try {
+              await collectionsService.removeFromCollection(
+                collection.id,
+                selectedFiles.files.toList(),
+              );
+              await dialog.hide();
+              selectedFiles.clearAll();
+            } catch (e, s) {
+              logger.severe(e, s);
+              await dialog.hide();
+              showGenericErrorDialog(context);
+            }
+          },
+          child: Text(showDeletePrompt ? "Yes, delete" : "Yes, remove"),
+        ),
+      ],
+      cancelButton: CupertinoActionSheetAction(
+        child: const Text("Cancel"),
+        onPressed: () {
+          Navigator.of(context, rootNavigator: true).pop();
+        },
+      ),
+    );
+    await showCupertinoModalPopup(context: context, builder: (_) => action);
+  }
+
+  // check if any of the file only belongs in the given collection id.
+  // if true, then we need to warn the user that some of the items will be
+  // deleted
+  Future<bool> _anyItemPresentOnlyInCurrentAlbum(
+    Set<File> files,
+    int collectionID,
+  ) async {
+    final List<int> uploadedIDs = files
+        .where((e) => e.uploadedFileID != null)
+        .map((e) => e.uploadedFileID!)
+        .toList();
+
+    final Map<int, List<File>> collectionToFilesMap =
+        await FilesDB.instance.getAllFilesGroupByCollectionID(uploadedIDs);
+    final Set<int> ids = uploadedIDs.toSet();
+    for (MapEntry<int, List<File>> entry in collectionToFilesMap.entries) {
+      if (entry.key == collectionID) {
+        logger.finest('ignore the collection from which remove is happening');
+        continue;
+      }
+      ids.removeAll(entry.value.map((f) => f.uploadedFileID!).toSet());
+    }
+    return ids.isNotEmpty;
+  }
+
+  Future<bool> updateFavorites(
+    BuildContext context,
+    List<File> files,
+    bool markAsFavorite,
+  ) async {
+    final ProgressDialog dialog = createProgressDialog(
+      context,
+      markAsFavorite ? "Adding to favorites..." : "Removing from favorites...",
+    );
+    await dialog.show();
+
+    try {
+      await FavoritesService.instance.updateFavorites(files, markAsFavorite);
+      return true;
+    } catch (e, s) {
+      logger.severe(e, s);
+      showShortToast(
+        context,
+        "Sorry, could not" +
+            (markAsFavorite ? "add  to favorites!" : "remove from favorites!"),
+      );
+    } finally {
+      await dialog.hide();
+    }
+    return false;
+  }
+}

+ 245 - 0
lib/ui/actions/collection/collection_sharing_actions.dart

@@ -0,0 +1,245 @@
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/user_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/common/dialogs.dart';
+import 'package:photos/ui/payment/subscription.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/email_util.dart';
+import 'package:photos/utils/share_util.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class CollectionActions {
+  final Logger logger = Logger((CollectionActions).toString());
+  final CollectionsService collectionsService;
+
+  CollectionActions(this.collectionsService);
+
+  Future<bool> publicLinkToggle(
+    BuildContext context,
+    Collection collection,
+    bool enable,
+  ) async {
+    // confirm if user wants to disable the url
+    if (!enable) {
+      final choice = await showChoiceDialog(
+        context,
+        'Remove public link?',
+        'This will remove the public link for accessing "${collection.name}".',
+        firstAction: 'Yes, remove',
+        secondAction: 'Cancel',
+        actionType: ActionType.critical,
+      );
+      if (choice != DialogUserChoice.firstChoice) {
+        return false;
+      }
+    }
+    final dialog = createProgressDialog(
+      context,
+      enable ? "Creating link..." : "Disabling link...",
+    );
+    try {
+      await dialog.show();
+      enable
+          ? await CollectionsService.instance.createShareUrl(collection)
+          : await CollectionsService.instance.disableShareUrl(collection);
+      dialog.hide();
+      return true;
+    } catch (e) {
+      dialog.hide();
+      if (e is SharingNotPermittedForFreeAccountsError) {
+        _showUnSupportedAlert(context);
+      } else {
+        logger.severe("Failed to update shareUrl collection", e);
+        showGenericErrorDialog(context);
+      }
+      return false;
+    }
+  }
+
+  // removeParticipant remove the user from a share album
+  Future<bool?> removeParticipant(
+    BuildContext context,
+    Collection collection,
+    User user,
+  ) async {
+    final result = await showChoiceDialog(
+      context,
+      "Remove?",
+      "${user.email} will be removed "
+          "from this shared album.\n\nAny photos and videos added by them will also be removed from the album.",
+      firstAction: "Yes, remove",
+      secondAction: "Cancel",
+      secondActionColor: getEnteColorScheme(context).strokeBase,
+      actionType: ActionType.critical,
+    );
+    if (result != DialogUserChoice.firstChoice) {
+      return Future.value(null);
+    }
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    try {
+      final newSharees =
+          await CollectionsService.instance.unshare(collection.id, user.email);
+      collection.updateSharees(newSharees);
+      await dialog.hide();
+      showToast(context, "Stopped sharing with " + user.email + ".");
+      return true;
+    } catch (e, s) {
+      Logger("EmailItemWidget").severe(e, s);
+      await dialog.hide();
+      await showGenericErrorDialog(context);
+      return false;
+    }
+  }
+
+  Future<bool?> addEmailToCollection(
+    BuildContext context,
+    Collection collection,
+    String email, {
+    CollectionParticipantRole role = CollectionParticipantRole.viewer,
+    String? publicKey,
+  }) async {
+    if (!isValidEmail(email)) {
+      await showErrorDialog(
+        context,
+        "Invalid email address",
+        "Please enter a valid email address.",
+      );
+      return null;
+    } else if (email == Configuration.instance.getEmail()) {
+      await showErrorDialog(context, "Oops", "You cannot share with yourself");
+      return null;
+    } else {
+      // if (collection.getSharees().any((user) => user.email == email)) {
+      //   showErrorDialog(
+      //     context,
+      //     "Oops",
+      //     "You're already sharing this with " + email,
+      //   );
+      //   return null;
+      // }
+    }
+    if (publicKey == null) {
+      final dialog = createProgressDialog(context, "Searching for user...");
+      await dialog.show();
+      try {
+        publicKey = await UserService.instance.getPublicKey(email);
+        await dialog.hide();
+      } catch (e) {
+        logger.severe("Failed to get public key", e);
+        showGenericErrorDialog(context);
+        await dialog.hide();
+      }
+    }
+    // getPublicKey can return null
+    // ignore: unnecessary_null_comparison
+    if (publicKey == null || publicKey == '') {
+      final dialog = AlertDialog(
+        title: const Text("Invite to ente?"),
+        content: Text(
+          "Looks like " +
+              email +
+              " hasn't signed up for ente yet. would you like to invite them?",
+          style: const TextStyle(
+            height: 1.4,
+          ),
+        ),
+        actions: [
+          TextButton(
+            child: Text(
+              "Invite",
+              style: TextStyle(
+                color: Theme.of(context).colorScheme.greenAlternative,
+              ),
+            ),
+            onPressed: () {
+              shareText(
+                "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.",
+              );
+            },
+          ),
+        ],
+      );
+      await showDialog(
+        context: context,
+        builder: (BuildContext context) {
+          return dialog;
+        },
+      );
+      return null;
+    } else {
+      final dialog = createProgressDialog(context, "Sharing...");
+      await dialog.show();
+      try {
+        final newSharees = await CollectionsService.instance
+            .share(collection.id, email, publicKey, role);
+        collection.updateSharees(newSharees);
+        await dialog.hide();
+        showShortToast(context, "Shared successfully!");
+        return true;
+      } catch (e) {
+        await dialog.hide();
+        if (e is SharingNotPermittedForFreeAccountsError) {
+          _showUnSupportedAlert(context);
+        } else {
+          logger.severe("failed to share collection", e);
+          showGenericErrorDialog(context);
+        }
+        return false;
+      }
+    }
+  }
+
+  void _showUnSupportedAlert(BuildContext context) {
+    final AlertDialog alert = AlertDialog(
+      title: const Text("Sorry"),
+      content: const Text(
+        "Looks like your subscription has expired. Please subscribe to enable"
+        " sharing.",
+      ),
+      actions: [
+        TextButton(
+          child: Text(
+            "Subscribe",
+            style: TextStyle(
+              color: Theme.of(context).colorScheme.greenAlternative,
+            ),
+          ),
+          onPressed: () {
+            Navigator.of(context, rootNavigator: true).pop();
+            Navigator.of(context).pushReplacement(
+              MaterialPageRoute(
+                builder: (BuildContext context) {
+                  return getSubscriptionPage();
+                },
+              ),
+            );
+          },
+        ),
+        TextButton(
+          child: Text(
+            "Ok",
+            style: TextStyle(
+              color: Theme.of(context).colorScheme.onSurface,
+            ),
+          ),
+          onPressed: () {
+            Navigator.of(context, rootNavigator: true).pop();
+          },
+        ),
+      ],
+    );
+
+    showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return alert;
+      },
+    );
+  }
+}

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

@@ -1,6 +1,8 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/db/files_db.dart';
+import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
+import 'package:photos/models/gallery_type.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
 import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
@@ -83,7 +85,15 @@ class CollectionItem extends StatelessWidget {
         ],
         ],
       ),
       ),
       onTap: () {
       onTap: () {
-        routeToPage(context, CollectionPage(c));
+        routeToPage(
+          context,
+          CollectionPage(
+            c,
+            appBarType: (c.collection.type == CollectionType.favorites
+                ? GalleryType.favorite
+                : GalleryType.ownedCollection),
+          ),
+        );
       },
       },
     );
     );
   }
   }

+ 7 - 1
lib/ui/common/gradient_button.dart

@@ -1,6 +1,7 @@
 // @dart=2.9
 // @dart=2.9
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
 
 
 class GradientButton extends StatelessWidget {
 class GradientButton extends StatelessWidget {
   final List<Color> linearGradientColors;
   final List<Color> linearGradientColors;
@@ -71,7 +72,12 @@ class GradientButton extends StatelessWidget {
           gradient: LinearGradient(
           gradient: LinearGradient(
             begin: const Alignment(0.1, -0.9),
             begin: const Alignment(0.1, -0.9),
             end: const Alignment(-0.6, 0.9),
             end: const Alignment(-0.6, 0.9),
-            colors: linearGradientColors,
+            colors: onTap != null
+                ? linearGradientColors
+                : [
+                    getEnteColorScheme(context).fillMuted,
+                    getEnteColorScheme(context).fillMuted
+                  ],
           ),
           ),
           borderRadius: BorderRadius.circular(8),
           borderRadius: BorderRadius.circular(8),
         ),
         ),

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

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

+ 183 - 0
lib/ui/components/action_sheet_widget.dart

@@ -0,0 +1,183 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/utils/separators_util.dart';
+
+enum ActionSheetType {
+  defaultActionSheet,
+  iconOnly,
+}
+
+Future<dynamic> showActionSheet({
+  required BuildContext context,
+  required List<ButtonWidget> buttons,
+  required ActionSheetType actionSheetType,
+  bool isCheckIconGreen = false,
+  String? title,
+  String? body,
+}) {
+  return showMaterialModalBottomSheet(
+    backgroundColor: Colors.transparent,
+    barrierColor: backdropFaintDark,
+    useRootNavigator: true,
+    context: context,
+    builder: (_) {
+      return ActionSheetWidget(
+        title: title,
+        body: body,
+        actionButtons: buttons,
+        actionSheetType: actionSheetType,
+        isCheckIconGreen: isCheckIconGreen,
+      );
+    },
+    isDismissible: false,
+    enableDrag: false,
+  );
+}
+
+class ActionSheetWidget extends StatelessWidget {
+  final String? title;
+  final String? body;
+  final List<ButtonWidget> actionButtons;
+  final ActionSheetType actionSheetType;
+  final bool isCheckIconGreen;
+
+  const ActionSheetWidget({
+    required this.actionButtons,
+    required this.actionSheetType,
+    required this.isCheckIconGreen,
+    this.title,
+    this.body,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final isTitleAndBodyNull = title == null && body == null;
+    final blur = MediaQuery.of(context).platformBrightness == Brightness.light
+        ? blurMuted
+        : blurBase;
+    final extraWidth = MediaQuery.of(context).size.width - restrictedMaxWidth;
+    final double? horizontalPadding = extraWidth > 0 ? extraWidth / 2 : null;
+    return Padding(
+      padding: EdgeInsets.fromLTRB(
+        horizontalPadding ?? 12,
+        12,
+        horizontalPadding ?? 12,
+        32,
+      ),
+      child: Container(
+        decoration: BoxDecoration(boxShadow: shadowMenuLight),
+        child: ClipRRect(
+          borderRadius: const BorderRadius.all(Radius.circular(8)),
+          child: BackdropFilter(
+            filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
+            child: Container(
+              color: backdropMutedDark,
+              child: Padding(
+                padding: EdgeInsets.fromLTRB(
+                  24,
+                  24,
+                  24,
+                  isTitleAndBodyNull ? 24 : 28,
+                ),
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    isTitleAndBodyNull
+                        ? const SizedBox.shrink()
+                        : Padding(
+                            padding: const EdgeInsets.only(bottom: 36),
+                            child: ContentContainerWidget(
+                              title: title,
+                              body: body,
+                              actionSheetType: actionSheetType,
+                              isCheckIconGreen: isCheckIconGreen,
+                            ),
+                          ),
+                    ActionButtons(
+                      actionButtons,
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class ContentContainerWidget extends StatelessWidget {
+  final String? title;
+  final String? body;
+  final ActionSheetType actionSheetType;
+  final bool isCheckIconGreen;
+  const ContentContainerWidget({
+    required this.actionSheetType,
+    required this.isCheckIconGreen,
+    this.title,
+    this.body,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context);
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      //todo: set cross axis to center when icon should be shown in place of body
+      crossAxisAlignment: actionSheetType == ActionSheetType.defaultActionSheet
+          ? CrossAxisAlignment.stretch
+          : CrossAxisAlignment.center,
+      children: [
+        title == null
+            ? const SizedBox.shrink()
+            : Text(
+                title!,
+                style: textTheme.h3Bold
+                    .copyWith(color: textBaseDark), //constant color
+              ),
+        title == null || body == null
+            ? const SizedBox.shrink()
+            : const SizedBox(height: 19),
+        actionSheetType == ActionSheetType.defaultActionSheet
+            ? body == null
+                ? const SizedBox.shrink()
+                : Text(
+                    body!,
+                    style: textTheme.body
+                        .copyWith(color: textMutedDark), //constant color
+                  )
+            : Icon(
+                Icons.check_outlined,
+                size: 48,
+                color: isCheckIconGreen
+                    ? getEnteColorScheme(context).primary700
+                    : strokeBaseDark,
+              )
+      ],
+    );
+  }
+}
+
+class ActionButtons extends StatelessWidget {
+  final List<Widget> actionButtons;
+  const ActionButtons(this.actionButtons, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final actionButtonsWithSeparators = actionButtons;
+    return Column(
+      children:
+          addSeparators(actionButtonsWithSeparators, const SizedBox(height: 8)),
+    );
+  }
+}

+ 120 - 0
lib/ui/components/blur_menu_item_widget.dart

@@ -0,0 +1,120 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class BlurMenuItemWidget extends StatefulWidget {
+  final IconData? leadingIcon;
+  final String? labelText;
+  final Color? menuItemColor;
+  final Color? pressedColor;
+  final VoidCallback? onTap;
+  const BlurMenuItemWidget({
+    this.leadingIcon,
+    this.labelText,
+    this.menuItemColor,
+    this.pressedColor,
+    this.onTap,
+    super.key,
+  });
+
+  @override
+  State<BlurMenuItemWidget> createState() => _BlurMenuItemWidgetState();
+}
+
+class _BlurMenuItemWidgetState extends State<BlurMenuItemWidget> {
+  Color? menuItemColor;
+  bool isDisabled = false;
+
+  @override
+  void initState() {
+    menuItemColor = widget.menuItemColor;
+    isDisabled = (widget.onTap == null);
+    super.initState();
+  }
+
+  @override
+  void didChangeDependencies() {
+    menuItemColor = widget.menuItemColor;
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    isDisabled = (widget.onTap == null);
+    final colorScheme = getEnteColorScheme(context);
+    return GestureDetector(
+      onTap: widget.onTap,
+      onTapDown: _onTapDown,
+      onTapUp: _onTapUp,
+      onTapCancel: _onCancel,
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 20),
+        color: isDisabled ? colorScheme.fillFaint : menuItemColor,
+        padding: const EdgeInsets.only(left: 16, right: 12),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 14),
+          child: Row(
+            children: [
+              widget.leadingIcon != null
+                  ? Padding(
+                      padding: const EdgeInsets.only(right: 10),
+                      child: Icon(
+                        widget.leadingIcon,
+                        size: 20,
+                        color: isDisabled
+                            ? colorScheme.strokeMuted
+                            : colorScheme.blurStrokeBase,
+                      ),
+                    )
+                  : const SizedBox.shrink(),
+              widget.labelText != null
+                  ? Flexible(
+                      child: Padding(
+                        padding: const EdgeInsets.symmetric(horizontal: 2),
+                        child: Row(
+                          children: [
+                            Flexible(
+                              child: Text(
+                                widget.labelText!,
+                                overflow: TextOverflow.ellipsis,
+                                maxLines: 1,
+                                style:
+                                    getEnteTextTheme(context).bodyBold.copyWith(
+                                          color: isDisabled
+                                              ? colorScheme.textFaint
+                                              : colorScheme.blurTextBase,
+                                        ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                    )
+                  : const SizedBox.shrink(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  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;
+    });
+  }
+}

+ 124 - 0
lib/ui/components/bottom_action_bar/action_bar_widget.dart

@@ -0,0 +1,124 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class ActionBarWidget extends StatefulWidget {
+  final String? text;
+  final List<Widget> iconButtons;
+  final SelectedFiles? selectedFiles;
+  final GalleryType galleryType;
+  final bool isCollaborator;
+
+  const ActionBarWidget({
+    required this.iconButtons,
+    required this.galleryType,
+    this.text,
+    this.selectedFiles,
+    this.isCollaborator = false,
+    super.key,
+  });
+
+  @override
+  State<ActionBarWidget> createState() => _ActionBarWidgetState();
+}
+
+class _ActionBarWidgetState extends State<ActionBarWidget> {
+  final ValueNotifier<int> _selectedFilesNotifier = ValueNotifier(0);
+  final ValueNotifier<int> _selectedOwnedFilesNotifier = ValueNotifier(0);
+  final int currentUserID = Configuration.instance.getUserID()!;
+
+  @override
+  void initState() {
+    widget.selectedFiles?.addListener(_selectedFilesListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.selectedFiles?.removeListener(_selectedFilesListener);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: _actionBarWidgets(context),
+      ),
+    );
+  }
+
+  List<Widget> _actionBarWidgets(BuildContext context) {
+    final actionBarWidgets = <Widget>[];
+    final initialLength = widget.iconButtons.length;
+    final textTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+
+    actionBarWidgets.addAll(widget.iconButtons);
+    if (widget.text != null) {
+      //adds 12 px spacing at the start and between iconButton elements
+      for (var i = 0; i < initialLength; i++) {
+        actionBarWidgets.insert(
+          2 * i,
+          const SizedBox(
+            width: 12,
+          ),
+        );
+      }
+      actionBarWidgets.insertAll(0, [
+        const SizedBox(width: 20),
+        Flexible(
+          child: Row(
+            children: [
+              widget.selectedFiles != null
+                  ? ValueListenableBuilder(
+                      valueListenable: _selectedFilesNotifier,
+                      builder: (context, value, child) {
+                        return Text(
+                          "${_selectedFilesNotifier.value} selected" +
+                              (_selectedOwnedFilesNotifier.value !=
+                                      _selectedFilesNotifier.value
+                                  ? " (${_selectedOwnedFilesNotifier.value} "
+                                      "yours) "
+                                  : ""),
+                          style: textTheme.body.copyWith(
+                            color: colorScheme.blurTextBase,
+                          ),
+                        );
+                      },
+                    )
+                  : Text(
+                      widget.text!,
+                      style:
+                          textTheme.body.copyWith(color: colorScheme.textMuted),
+                    ),
+            ],
+          ),
+        ),
+      ]);
+      //to add whitespace of 8pts or 12 pts at the end
+      if (widget.iconButtons.length > 1) {
+        actionBarWidgets.add(
+          const SizedBox(width: 8),
+        );
+      } else {
+        actionBarWidgets.add(
+          const SizedBox(width: 12),
+        );
+      }
+    }
+    return actionBarWidgets;
+  }
+
+  void _selectedFilesListener() {
+    if (widget.selectedFiles!.files.isNotEmpty) {
+      _selectedFilesNotifier.value = widget.selectedFiles!.files.length;
+      _selectedOwnedFilesNotifier.value = widget.selectedFiles!.files
+          .where((f) => f.ownerID == null || f.ownerID! == currentUserID)
+          .length;
+    }
+  }
+}

+ 184 - 0
lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart

@@ -0,0 +1,184 @@
+import 'dart:ui';
+
+import 'package:expandable/expandable.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/bottom_action_bar/action_bar_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+
+class BottomActionBarWidget extends StatelessWidget {
+  final String? text;
+  final List<Widget>? iconButtons;
+  final Widget expandedMenu;
+  final SelectedFiles? selectedFiles;
+  final VoidCallback? onCancel;
+  final bool hasSmallerBottomPadding;
+  final GalleryType type;
+
+  BottomActionBarWidget({
+    required this.expandedMenu,
+    required this.hasSmallerBottomPadding,
+    required this.type,
+    this.selectedFiles,
+    this.text,
+    this.iconButtons,
+    this.onCancel,
+    super.key,
+  });
+
+  final ExpandableController _expandableController =
+      ExpandableController(initialExpanded: false);
+
+  @override
+  Widget build(BuildContext context) {
+    final widthOfScreen = MediaQuery.of(context).size.width;
+    final colorScheme = getEnteColorScheme(context);
+    final textTheme = getEnteTextTheme(context);
+    final double leftRightPadding = widthOfScreen > restrictedMaxWidth
+        ? (widthOfScreen - restrictedMaxWidth) / 2
+        : 0;
+    return ClipRRect(
+      child: BackdropFilter(
+        filter: ImageFilter.blur(sigmaX: blurFaint, sigmaY: blurFaint),
+        child: Container(
+          color: colorScheme.backdropBase,
+          padding: EdgeInsets.only(
+            top: 4,
+            bottom: hasSmallerBottomPadding ? 24 : 36,
+            right: leftRightPadding,
+            left: leftRightPadding,
+          ),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              ExpandableNotifier(
+                controller: _expandableController,
+                child: ExpandablePanel(
+                  theme: _getExpandableTheme(),
+                  header: Padding(
+                    padding: EdgeInsets.symmetric(
+                      horizontal: text == null ? 12 : 0,
+                    ),
+                    child: ActionBarWidget(
+                      selectedFiles: selectedFiles,
+                      galleryType: type,
+                      text: text,
+                      iconButtons: _iconButtons(context),
+                    ),
+                  ),
+                  expanded: expandedMenu,
+                  collapsed: const SizedBox.shrink(),
+                  controller: _expandableController,
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 14,
+                ),
+                child: GestureDetector(
+                  behavior: HitTestBehavior.opaque,
+                  onTap: () {
+                    onCancel?.call();
+                    _expandableController.value = false;
+                  },
+                  child: Center(
+                    child: Text(
+                      "Cancel",
+                      style: textTheme.bodyBold
+                          .copyWith(color: colorScheme.blurTextBase),
+                    ),
+                  ),
+                ),
+              )
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  List<Widget> _iconButtons(BuildContext context) {
+    final iconButtonsWithExpansionIcon = <Widget?>[
+      ...?iconButtons,
+      ExpansionIconWidget(expandableController: _expandableController)
+    ];
+    iconButtonsWithExpansionIcon.removeWhere((element) => element == null);
+    return iconButtonsWithExpansionIcon as List<Widget>;
+  }
+
+  ExpandableThemeData _getExpandableTheme() {
+    return const ExpandableThemeData(
+      hasIcon: false,
+      useInkWell: false,
+      tapBodyToCollapse: false,
+      tapBodyToExpand: false,
+      tapHeaderToExpand: false,
+      animationDuration: Duration(milliseconds: 400),
+      crossFadePoint: 0.5,
+    );
+  }
+}
+
+class ExpansionIconWidget extends StatefulWidget {
+  final ExpandableController expandableController;
+
+  const ExpansionIconWidget({required this.expandableController, super.key});
+
+  @override
+  State<ExpansionIconWidget> createState() => _ExpansionIconWidgetState();
+}
+
+class _ExpansionIconWidgetState extends State<ExpansionIconWidget> {
+  @override
+  void initState() {
+    widget.expandableController.addListener(_expandableListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.expandableController.removeListener(_expandableListener);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final iconColor = getEnteColorScheme(context).blurStrokeBase;
+    return AnimatedSwitcher(
+      duration: const Duration(milliseconds: 200),
+      switchInCurve: Curves.easeInOutExpo,
+      child: widget.expandableController.value
+          ? IconButtonWidget(
+              key: const ValueKey<bool>(false),
+              onTap: () {
+                widget.expandableController.value = false;
+                setState(() {});
+              },
+              icon: Icons.expand_more_outlined,
+              iconButtonType: IconButtonType.primary,
+              iconColor: iconColor,
+            )
+          : IconButtonWidget(
+              key: const ValueKey<bool>(true),
+              onTap: () {
+                widget.expandableController.value = true;
+                setState(() {});
+              },
+              icon: Icons.more_horiz_outlined,
+              iconButtonType: IconButtonType.primary,
+              iconColor: iconColor,
+            ),
+    );
+  }
+
+  _expandableListener() {
+    if (mounted) {
+      setState(() {});
+    }
+  }
+}

+ 71 - 0
lib/ui/components/bottom_action_bar/expanded_menu_widget.dart

@@ -0,0 +1,71 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ui/components/blur_menu_item_widget.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+
+class ExpandedMenuWidget extends StatelessWidget {
+  final List<List<BlurMenuItemWidget>> items;
+  const ExpandedMenuWidget({
+    required this.items,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+    textScaleFactor < 1.0 ? textScaleFactor = 1.0 : null;
+    //20 is height of font and 28 is total whitespace (top+bottom)
+    final double itemHeight = (20.0 * textScaleFactor) + 28.0;
+    const double whiteSpaceBetweenSections = 16.0;
+    const double dividerHeightBetweenItems = 1.0;
+    double numberOfDividers = 0.0;
+    double combinedHeightOfItems = 0.0;
+
+    for (List<BlurMenuItemWidget> group in items) {
+      //no divider if there is only one item in the section/group
+      if (group.length != 1) {
+        numberOfDividers += (group.length - 1);
+      }
+      combinedHeightOfItems += group.length * itemHeight;
+    }
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+      child: SizedBox(
+        height: combinedHeightOfItems +
+            (dividerHeightBetweenItems * numberOfDividers) +
+            (whiteSpaceBetweenSections * (items.length - 1.0)),
+        child: ListView.separated(
+          padding: const EdgeInsets.all(0),
+          physics: const NeverScrollableScrollPhysics(),
+          itemBuilder: (context, sectionIndex) {
+            return ClipRRect(
+              borderRadius: const BorderRadius.all(Radius.circular(8)),
+              child: SizedBox(
+                height: itemHeight * items[sectionIndex].length +
+                    (dividerHeightBetweenItems *
+                        (items[sectionIndex].length - 1)),
+                child: ListView.separated(
+                  padding: const EdgeInsets.all(0),
+                  physics: const NeverScrollableScrollPhysics(),
+                  itemBuilder: (context, itemIndex) {
+                    return items[sectionIndex][itemIndex];
+                  },
+                  separatorBuilder: (context, index) {
+                    return const DividerWidget(
+                      dividerType: DividerType.bottomBar,
+                    );
+                  },
+                  itemCount: items[sectionIndex].length,
+                ),
+              ),
+            );
+          },
+          separatorBuilder: (context, index) {
+            return const SizedBox(height: whiteSpaceBetweenSections);
+          },
+          itemCount: items.length,
+        ),
+      ),
+    );
+  }
+}

+ 425 - 0
lib/ui/components/button_widget.dart

@@ -0,0 +1,425 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/theme/text_style.dart';
+import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/components/models/button_type.dart';
+import 'package:photos/ui/components/models/custom_button_style.dart';
+import 'package:photos/utils/debouncer.dart';
+
+enum ExecutionState {
+  idle,
+  inProgress,
+  error,
+  successful;
+}
+
+enum ButtonSize {
+  small,
+  large;
+}
+
+enum ButtonAction {
+  first,
+  second,
+  third,
+  cancel,
+  error;
+}
+
+typedef FutureVoidCallback = Future<void> Function();
+
+class ButtonWidget extends StatelessWidget {
+  final IconData? icon;
+  final String? labelText;
+  final ButtonType buttonType;
+  final FutureVoidCallback? onTap;
+  final bool isDisabled;
+  final ButtonSize buttonSize;
+
+  /// iconColor should only be specified when we do not want to honor the default
+  /// iconColor based on buttonType. Most of the items, default iconColor is what
+  /// we need unless we want to pop out the icon in a non-primary button type
+  final Color? iconColor;
+
+  ///Button action will only work if isInAlert is true
+  final ButtonAction? buttonAction;
+
+  ///setting this flag to true will make the button appear like how it would
+  ///on dark theme irrespective of the app's theme.
+  final bool shouldStickToDarkTheme;
+
+  ///isInAlert is to dismiss the alert if the action on the button is completed.
+  ///This should be set to true if the alert which uses this button needs to
+  ///return the Button's action.
+  final bool isInAlert;
+  const ButtonWidget({
+    required this.buttonType,
+    required this.buttonSize,
+    this.icon,
+    this.labelText,
+    this.onTap,
+    this.shouldStickToDarkTheme = false,
+    this.isDisabled = false,
+    this.buttonAction,
+    this.isInAlert = false,
+    this.iconColor,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme =
+        shouldStickToDarkTheme ? darkScheme : getEnteColorScheme(context);
+    final inverseColorScheme = shouldStickToDarkTheme
+        ? lightScheme
+        : getEnteColorScheme(context, inverse: true);
+    final textTheme =
+        shouldStickToDarkTheme ? darkTextTheme : getEnteTextTheme(context);
+    final inverseTextTheme = shouldStickToDarkTheme
+        ? lightTextTheme
+        : getEnteTextTheme(context, inverse: true);
+    final buttonStyle = CustomButtonStyle(
+      //Dummy default values since we need to keep these properties non-nullable
+      defaultButtonColor: Colors.transparent,
+      defaultIconColor: Colors.transparent,
+      defaultLabelStyle: textTheme.body,
+    );
+    buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
+    buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
+    buttonStyle.disabledButtonColor =
+        buttonType.disabledButtonColor(colorScheme, buttonSize);
+    buttonStyle.defaultBorderColor =
+        buttonType.defaultBorderColor(colorScheme, buttonSize);
+    buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(
+      colorScheme: colorScheme,
+      inverseColorScheme: inverseColorScheme,
+      buttonSize: buttonSize,
+    );
+    buttonStyle.disabledBorderColor =
+        buttonType.disabledBorderColor(colorScheme, buttonSize);
+    buttonStyle.defaultIconColor = iconColor ??
+        buttonType.defaultIconColor(
+          colorScheme: colorScheme,
+          inverseColorScheme: inverseColorScheme,
+        );
+    buttonStyle.pressedIconColor =
+        buttonType.pressedIconColor(colorScheme, buttonSize);
+    buttonStyle.disabledIconColor =
+        buttonType.disabledIconColor(colorScheme, buttonSize);
+    buttonStyle.defaultLabelStyle = buttonType.defaultLabelStyle(
+      textTheme: textTheme,
+      inverseTextTheme: inverseTextTheme,
+    );
+    buttonStyle.pressedLabelStyle =
+        buttonType.pressedLabelStyle(textTheme, colorScheme, buttonSize);
+    buttonStyle.disabledLabelStyle =
+        buttonType.disabledLabelStyle(textTheme, colorScheme);
+    buttonStyle.checkIconColor = buttonType.checkIconColor(colorScheme);
+
+    return ButtonChildWidget(
+      buttonStyle: buttonStyle,
+      buttonType: buttonType,
+      isDisabled: isDisabled,
+      buttonSize: buttonSize,
+      isInAlert: isInAlert,
+      onTap: onTap,
+      labelText: labelText,
+      icon: icon,
+      buttonAction: buttonAction,
+    );
+  }
+}
+
+class ButtonChildWidget extends StatefulWidget {
+  final CustomButtonStyle buttonStyle;
+  final FutureVoidCallback? onTap;
+  final ButtonType buttonType;
+  final String? labelText;
+  final IconData? icon;
+  final bool isDisabled;
+  final ButtonSize buttonSize;
+  final ButtonAction? buttonAction;
+  final bool isInAlert;
+  const ButtonChildWidget({
+    required this.buttonStyle,
+    required this.buttonType,
+    required this.isDisabled,
+    required this.buttonSize,
+    required this.isInAlert,
+    this.onTap,
+    this.labelText,
+    this.icon,
+    this.buttonAction,
+    super.key,
+  });
+
+  @override
+  State<ButtonChildWidget> createState() => _ButtonChildWidgetState();
+}
+
+class _ButtonChildWidgetState extends State<ButtonChildWidget> {
+  late Color buttonColor;
+  late Color? borderColor;
+  late Color iconColor;
+  late TextStyle labelStyle;
+  late Color checkIconColor;
+  late Color loadingIconColor;
+
+  ///This is used to store the width of the button in idle state (small button)
+  ///to be used as width for the button when the loading/succes states comes.
+  double? widthOfButton;
+  final _debouncer = Debouncer(const Duration(milliseconds: 300));
+  ExecutionState executionState = ExecutionState.idle;
+  @override
+  void initState() {
+    checkIconColor = widget.buttonStyle.checkIconColor ??
+        widget.buttonStyle.defaultIconColor;
+    loadingIconColor = widget.buttonStyle.defaultIconColor;
+    if (widget.isDisabled) {
+      buttonColor = widget.buttonStyle.disabledButtonColor ??
+          widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.disabledBorderColor ??
+          widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.disabledIconColor ??
+          widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.disabledLabelStyle ??
+          widget.buttonStyle.defaultLabelStyle;
+    } else {
+      buttonColor = widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.defaultLabelStyle;
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: _shouldRegisterGestures ? _onTap : null,
+      onTapDown: _shouldRegisterGestures ? _onTapDown : null,
+      onTapUp: _shouldRegisterGestures ? _onTapUp : null,
+      onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 16),
+        width: widget.buttonSize == ButtonSize.large ? double.infinity : null,
+        decoration: BoxDecoration(
+          borderRadius: const BorderRadius.all(Radius.circular(4)),
+          color: buttonColor,
+          border: borderColor != null
+              ? Border.all(
+                  color: borderColor!,
+                  // strokeAlign: StrokeAlign.outside,
+                )
+              : null,
+        ),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
+          child: AnimatedSwitcher(
+            duration: const Duration(milliseconds: 175),
+            switchInCurve: Curves.easeInOutExpo,
+            switchOutCurve: Curves.easeInOutExpo,
+            child: executionState == ExecutionState.idle
+                ? widget.buttonType.hasTrailingIcon
+                    ? Row(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        children: [
+                          widget.labelText == null
+                              ? const SizedBox.shrink()
+                              : Flexible(
+                                  child: Padding(
+                                    padding: widget.icon == null
+                                        ? const EdgeInsets.symmetric(
+                                            horizontal: 8,
+                                          )
+                                        : const EdgeInsets.only(right: 16),
+                                    child: Text(
+                                      widget.labelText!,
+                                      overflow: TextOverflow.ellipsis,
+                                      maxLines: 2,
+                                      style: labelStyle,
+                                    ),
+                                  ),
+                                ),
+                          widget.icon == null
+                              ? const SizedBox.shrink()
+                              : Icon(
+                                  widget.icon,
+                                  size: 20,
+                                  color: iconColor,
+                                ),
+                        ],
+                      )
+                    : Builder(
+                        builder: (context) {
+                          SchedulerBinding.instance.addPostFrameCallback(
+                            (timeStamp) {
+                              final box =
+                                  context.findRenderObject() as RenderBox;
+                              widthOfButton = box.size.width;
+                            },
+                          );
+                          return Row(
+                            mainAxisSize: widget.buttonSize == ButtonSize.large
+                                ? MainAxisSize.max
+                                : MainAxisSize.min,
+                            mainAxisAlignment: MainAxisAlignment.center,
+                            children: [
+                              widget.icon == null
+                                  ? const SizedBox.shrink()
+                                  : Icon(
+                                      widget.icon,
+                                      size: 20,
+                                      color: iconColor,
+                                    ),
+                              widget.icon == null || widget.labelText == null
+                                  ? const SizedBox.shrink()
+                                  : const SizedBox(width: 8),
+                              widget.labelText == null
+                                  ? const SizedBox.shrink()
+                                  : Flexible(
+                                      child: Padding(
+                                        padding: const EdgeInsets.symmetric(
+                                          horizontal: 8,
+                                        ),
+                                        child: Text(
+                                          widget.labelText!,
+                                          style: labelStyle,
+                                          maxLines: 2,
+                                          overflow: TextOverflow.ellipsis,
+                                        ),
+                                      ),
+                                    )
+                            ],
+                          );
+                        },
+                      )
+                : executionState == ExecutionState.inProgress
+                    ? SizedBox(
+                        width: widthOfButton,
+                        child: Row(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          mainAxisSize: MainAxisSize.min,
+                          children: [
+                            EnteLoadingWidget(
+                              is20pts: true,
+                              color: loadingIconColor,
+                            ),
+                          ],
+                        ),
+                      )
+                    : executionState == ExecutionState.successful
+                        ? SizedBox(
+                            width: widthOfButton,
+                            child: Icon(
+                              Icons.check_outlined,
+                              size: 20,
+                              color: checkIconColor,
+                            ),
+                          )
+                        : const SizedBox.shrink(), //fallback
+          ),
+        ),
+      ),
+    );
+  }
+
+  bool get _shouldRegisterGestures =>
+      !widget.isDisabled && executionState == ExecutionState.idle;
+
+  void _onTap() async {
+    if (widget.onTap != null) {
+      _debouncer.run(
+        () => Future(() {
+          setState(() {
+            executionState = ExecutionState.inProgress;
+          });
+        }),
+      );
+      await widget.onTap!.call().onError((error, stackTrace) {
+        executionState = ExecutionState.error;
+        _debouncer.cancelDebounce();
+      });
+      _debouncer.cancelDebounce();
+      // when the time taken by widget.onTap is approximately equal to the debounce
+      // time, the callback is getting executed when/after the if condition
+      // below is executing/executed which results in execution state stuck at
+      // idle state. This Future is for delaying the execution of the if
+      // condition so that the calback in the debouncer finishes execution before.
+      await Future.delayed(const Duration(milliseconds: 5));
+    }
+    if (executionState == ExecutionState.inProgress ||
+        executionState == ExecutionState.error) {
+      if (executionState == ExecutionState.inProgress) {
+        setState(() {
+          executionState = ExecutionState.successful;
+          Future.delayed(Duration(seconds: widget.isInAlert ? 1 : 2), () {
+            widget.isInAlert
+                ? Navigator.of(context, rootNavigator: true)
+                    .pop(widget.buttonAction)
+                : null;
+            if (mounted) {
+              setState(() {
+                executionState = ExecutionState.idle;
+              });
+            }
+          });
+        });
+      }
+      if (executionState == ExecutionState.error) {
+        setState(() {
+          executionState = ExecutionState.idle;
+          widget.isInAlert
+              ? Future.delayed(
+                  const Duration(seconds: 0),
+                  () => Navigator.of(context, rootNavigator: true).pop(
+                    ButtonAction.error,
+                  ),
+                )
+              : null;
+        });
+      }
+    } else {
+      if (widget.isInAlert) {
+        Navigator.of(context).pop(widget.buttonAction);
+      }
+    }
+  }
+
+  void _onTapDown(details) {
+    setState(() {
+      buttonColor = widget.buttonStyle.pressedButtonColor ??
+          widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.pressedBorderColor ??
+          widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.pressedIconColor ??
+          widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.pressedLabelStyle ??
+          widget.buttonStyle.defaultLabelStyle;
+    });
+  }
+
+  void _onTapUp(details) {
+    Future.delayed(
+      const Duration(milliseconds: 84),
+      () => setState(() {
+        setAllStylesToDefault();
+      }),
+    );
+  }
+
+  void _onTapCancel() {
+    setState(() {
+      setAllStylesToDefault();
+    });
+  }
+
+  void setAllStylesToDefault() {
+    buttonColor = widget.buttonStyle.defaultButtonColor;
+    borderColor = widget.buttonStyle.defaultBorderColor;
+    iconColor = widget.buttonStyle.defaultIconColor;
+    labelStyle = widget.buttonStyle.defaultLabelStyle;
+  }
+}

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

@@ -40,7 +40,7 @@ class CaptionedTextWidget extends StatelessWidget {
         Text(
         Text(
           '\u2022',
           '\u2022',
           style: enteTextTheme.small.copyWith(
           style: enteTextTheme.small.copyWith(
-            color: enteColorScheme.textMuted,
+            color: subTitleColor ?? enteColorScheme.textMuted,
           ),
           ),
         ),
         ),
       );
       );

+ 146 - 0
lib/ui/components/dialog_widget.dart

@@ -0,0 +1,146 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/utils/separators_util.dart';
+
+Future<dynamic> showDialogWidget({
+  required BuildContext context,
+  required String title,
+  required String body,
+  required List<ButtonWidget> buttons,
+  IconData? icon,
+}) {
+  return showDialog(
+    barrierDismissible: false,
+    barrierColor: backdropFaintDark,
+    context: context,
+    builder: (context) {
+      final widthOfScreen = MediaQuery.of(context).size.width;
+      final isMobileSmall = widthOfScreen <= mobileSmallThreshold;
+      return Padding(
+        padding: EdgeInsets.symmetric(horizontal: isMobileSmall ? 8 : 0),
+        child: Dialog(
+          insetPadding: EdgeInsets.zero,
+          child: DialogWidget(
+            title: title,
+            body: body,
+            buttons: buttons,
+            isMobileSmall: isMobileSmall,
+            icon: icon,
+          ),
+        ),
+      );
+    },
+  );
+}
+
+class DialogWidget extends StatelessWidget {
+  final String title;
+  final String body;
+  final List<ButtonWidget> buttons;
+  final IconData? icon;
+  final bool isMobileSmall;
+  const DialogWidget({
+    required this.title,
+    required this.body,
+    required this.buttons,
+    required this.isMobileSmall,
+    this.icon,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final widthOfScreen = MediaQuery.of(context).size.width;
+    final colorScheme = getEnteColorScheme(context);
+    return Container(
+      width: min(widthOfScreen, 320),
+      padding: isMobileSmall
+          ? const EdgeInsets.all(0)
+          : const EdgeInsets.fromLTRB(6, 8, 6, 6),
+      decoration: BoxDecoration(
+        color: colorScheme.backgroundElevated,
+        boxShadow: shadowFloatLight,
+        borderRadius: const BorderRadius.all(Radius.circular(8)),
+      ),
+      child: Padding(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            ContentContainer(
+              title: title,
+              body: body,
+              icon: icon,
+            ),
+            const SizedBox(height: 36),
+            Actions(buttons),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class ContentContainer extends StatelessWidget {
+  final String title;
+  final String body;
+  final IconData? icon;
+  const ContentContainer({
+    required this.title,
+    required this.body,
+    this.icon,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: [
+        icon == null
+            ? const SizedBox.shrink()
+            : Row(
+                children: [
+                  Icon(
+                    icon,
+                    size: 48,
+                  ),
+                ],
+              ),
+        icon == null ? const SizedBox.shrink() : const SizedBox(height: 19),
+        Text(title, style: textTheme.h3Bold),
+        const SizedBox(height: 19),
+        Text(
+          body,
+          style: textTheme.body.copyWith(color: colorScheme.textMuted),
+        ),
+      ],
+    );
+  }
+}
+
+class Actions extends StatelessWidget {
+  final List<ButtonWidget> buttons;
+  const Actions(this.buttons, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: addSeparators(
+        buttons,
+        const SizedBox(
+          height: 8,
+        ),
+      ),
+    );
+  }
+}

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

@@ -71,7 +71,7 @@ class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
                 padding: const EdgeInsets.only(bottom: 4),
                 padding: const EdgeInsets.only(bottom: 4),
                 child: widget.selectionOptionsWidget,
                 child: widget.selectionOptionsWidget,
               ),
               ),
-              theme: getExpandableTheme(context),
+              theme: getExpandableTheme(),
               controller: expandableController,
               controller: expandableController,
             ),
             ),
           ),
           ),

+ 0 - 3
lib/ui/components/home_header_widget.dart

@@ -1,6 +1,4 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:photos/core/event_bus.dart';
-import 'package:photos/events/opened_settings_event.dart';
 import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/viewer/search/search_widget.dart';
 import 'package:photos/ui/viewer/search/search_widget.dart';
 
 
@@ -24,7 +22,6 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
           icon: Icons.menu_outlined,
           icon: Icons.menu_outlined,
           onTap: () {
           onTap: () {
             Scaffold.of(context).openDrawer();
             Scaffold.of(context).openDrawer();
-            Bus.instance.fire(OpenedSettingsEvent());
           },
           },
         ),
         ),
         AnimatedSwitcher(
         AnimatedSwitcher(

+ 18 - 4
lib/ui/components/menu_item_widget.dart

@@ -11,6 +11,10 @@ class MenuItemWidget extends StatefulWidget {
   final IconData? leadingIcon;
   final IconData? leadingIcon;
   final Color? leadingIconColor;
   final Color? leadingIconColor;
 
 
+  final Widget? leadingIconWidget;
+  // leadIconSize deafult value is 20.
+  final double leadingIconSize;
+
   /// trailing icon can be passed without size as default size set by
   /// trailing icon can be passed without size as default size set by
   /// flutter is what this component expects
   /// flutter is what this component expects
   final IconData? trailingIcon;
   final IconData? trailingIcon;
@@ -28,11 +32,14 @@ class MenuItemWidget extends StatefulWidget {
 
 
   /// disable gesture detector if not used
   /// disable gesture detector if not used
   final bool isGestureDetectorDisabled;
   final bool isGestureDetectorDisabled;
+
   const MenuItemWidget({
   const MenuItemWidget({
     required this.captionedTextWidget,
     required this.captionedTextWidget,
     this.isExpandable = false,
     this.isExpandable = false,
     this.leadingIcon,
     this.leadingIcon,
     this.leadingIconColor,
     this.leadingIconColor,
+    this.leadingIconSize = 20.0,
+    this.leadingIconWidget,
     this.trailingIcon,
     this.trailingIcon,
     this.trailingWidget,
     this.trailingWidget,
     this.trailingIconIsMuted = false,
     this.trailingIconIsMuted = false,
@@ -55,6 +62,7 @@ class MenuItemWidget extends StatefulWidget {
 
 
 class _MenuItemWidgetState extends State<MenuItemWidget> {
 class _MenuItemWidgetState extends State<MenuItemWidget> {
   Color? menuItemColor;
   Color? menuItemColor;
+
   @override
   @override
   void initState() {
   void initState() {
     menuItemColor = widget.menuItemColor;
     menuItemColor = widget.menuItemColor;
@@ -126,15 +134,21 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
               : Padding(
               : Padding(
                   padding: const EdgeInsets.only(right: 10),
                   padding: const EdgeInsets.only(right: 10),
                   child: SizedBox(
                   child: SizedBox(
-                    height: 20,
-                    width: 20,
+                    height: widget.leadingIconSize,
+                    width: widget.leadingIconSize,
                     child: widget.leadingIcon == null
                     child: widget.leadingIcon == null
-                        ? const SizedBox.shrink()
+                        ? (widget.leadingIconWidget != null
+                            ? FittedBox(
+                                fit: BoxFit.contain,
+                                child: widget.leadingIconWidget,
+                              )
+                            : const SizedBox.shrink())
                         : FittedBox(
                         : FittedBox(
                             fit: BoxFit.contain,
                             fit: BoxFit.contain,
                             child: Icon(
                             child: Icon(
                               widget.leadingIcon,
                               widget.leadingIcon,
-                              color: widget.leadingIconColor,
+                              color: widget.leadingIconColor ??
+                                  enteColorScheme.strokeBase,
                             ),
                             ),
                           ),
                           ),
                   ),
                   ),

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

@@ -11,6 +11,7 @@ class MenuSectionDescriptionWidget extends StatelessWidget {
       padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
       padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
       child: Text(
       child: Text(
         content,
         content,
+        textAlign: TextAlign.left,
         style: getEnteTextTheme(context)
         style: getEnteTextTheme(context)
             .mini
             .mini
             .copyWith(color: getEnteColorScheme(context).textMuted),
             .copyWith(color: getEnteColorScheme(context).textMuted),

+ 35 - 0
lib/ui/components/menu_section_title.dart

@@ -0,0 +1,35 @@
+import 'package:flutter/widgets.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class MenuSectionTitle extends StatelessWidget {
+  final String title;
+  final IconData? iconData;
+
+  const MenuSectionTitle({super.key, required this.title, this.iconData});
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    return Padding(
+      padding: const EdgeInsets.only(left: 8, top: 6, bottom: 6),
+      child: Row(
+        children: [
+          iconData != null
+              ? Icon(
+                  iconData,
+                  color: colorScheme.strokeMuted,
+                  size: 17,
+                )
+              : const SizedBox.shrink(),
+          iconData != null ? const SizedBox(width: 8) : const SizedBox.shrink(),
+          Text(
+            title,
+            style: getEnteTextTheme(context).small.copyWith(
+                  color: colorScheme.textMuted,
+                ),
+          )
+        ],
+      ),
+    );
+  }
+}

+ 203 - 0
lib/ui/components/models/button_type.dart

@@ -0,0 +1,203 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/text_style.dart';
+import 'package:photos/ui/components/button_widget.dart';
+
+enum ButtonType {
+  primary,
+  secondary,
+  neutral,
+  trailingIcon,
+  critical,
+  tertiaryCritical,
+  trailingIconPrimary,
+  trailingIconSecondary,
+  tertiary;
+
+  bool get isPrimary =>
+      this == ButtonType.primary || this == ButtonType.trailingIconPrimary;
+
+  bool get hasTrailingIcon =>
+      this == ButtonType.trailingIcon ||
+      this == ButtonType.trailingIconPrimary ||
+      this == ButtonType.trailingIconSecondary;
+
+  bool get isSecondary =>
+      this == ButtonType.secondary || this == ButtonType.trailingIconSecondary;
+
+  bool get isCritical =>
+      this == ButtonType.critical || this == ButtonType.tertiaryCritical;
+
+  bool get isNeutral =>
+      this == ButtonType.neutral || this == ButtonType.trailingIcon;
+
+  Color defaultButtonColor(EnteColorScheme colorScheme) {
+    if (isPrimary) {
+      return colorScheme.primary500;
+    }
+    if (isSecondary) {
+      return colorScheme.fillFaint;
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return colorScheme.fillBase;
+    }
+    if (this == ButtonType.critical) {
+      return colorScheme.warning700;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return Colors.transparent;
+    }
+    return Colors.transparent;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedButtonColor(EnteColorScheme colorScheme) {
+    if (isPrimary) {
+      return colorScheme.primary700;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledButtonColor(
+    EnteColorScheme colorScheme,
+    ButtonSize buttonSize,
+  ) {
+    if (buttonSize == ButtonSize.small &&
+        (this == ButtonType.primary ||
+            this == ButtonType.neutral ||
+            this == ButtonType.critical)) {
+      return colorScheme.fillMuted;
+    }
+    if (isPrimary || this == ButtonType.critical || isNeutral) {
+      return colorScheme.fillFaint;
+    }
+    return null;
+  }
+
+  Color? defaultBorderColor(
+    EnteColorScheme colorScheme,
+    ButtonSize buttonSize,
+  ) {
+    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
+      return colorScheme.warning700;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedBorderColor({
+    required EnteColorScheme colorScheme,
+    required EnteColorScheme inverseColorScheme,
+    required ButtonSize buttonSize,
+  }) {
+    if (isPrimary) {
+      return colorScheme.strokeMuted;
+    }
+    if (buttonSize == ButtonSize.small && this == ButtonType.tertiaryCritical) {
+      return null;
+    }
+    if (isSecondary || isCritical) {
+      return colorScheme.strokeBase;
+    }
+    if (isNeutral) {
+      return inverseColorScheme.strokeBase;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledBorderColor(
+    EnteColorScheme colorScheme,
+    ButtonSize buttonSize,
+  ) {
+    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
+      return colorScheme.strokeMuted;
+    }
+    return null;
+  }
+
+  Color defaultIconColor({
+    required EnteColorScheme colorScheme,
+    required EnteColorScheme inverseColorScheme,
+  }) {
+    if (isPrimary || this == ButtonType.critical) {
+      return strokeBaseDark;
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return inverseColorScheme.strokeBase;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.warning500;
+    }
+    //fallback
+    return colorScheme.strokeBase;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
+    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
+      return colorScheme.strokeBase;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
+    if (isPrimary ||
+        isSecondary ||
+        isNeutral ||
+        buttonSize == ButtonSize.small) {
+      return colorScheme.strokeMuted;
+    }
+    if (isCritical) {
+      return colorScheme.strokeFaint;
+    }
+    return null;
+  }
+
+  TextStyle defaultLabelStyle({
+    required EnteTextTheme textTheme,
+    required EnteTextTheme inverseTextTheme,
+  }) {
+    if (isPrimary || this == ButtonType.critical) {
+      return textTheme.bodyBold.copyWith(color: textBaseDark);
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return inverseTextTheme.bodyBold;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return textTheme.bodyBold.copyWith(color: warning500);
+    }
+    //fallback
+    return textTheme.bodyBold;
+  }
+
+  //Returning null to fallback to default color
+  TextStyle? pressedLabelStyle(
+    EnteTextTheme textTheme,
+    EnteColorScheme colorScheme,
+    ButtonSize buttonSize,
+  ) {
+    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
+      return textTheme.bodyBold.copyWith(color: colorScheme.strokeBase);
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  TextStyle? disabledLabelStyle(
+    EnteTextTheme textTheme,
+    EnteColorScheme colorScheme,
+  ) {
+    return textTheme.bodyBold.copyWith(color: colorScheme.textFaint);
+  }
+
+  //Returning null to fallback to default color
+  Color? checkIconColor(EnteColorScheme colorScheme) {
+    if (isSecondary) {
+      return colorScheme.primary500;
+    }
+    return null;
+  }
+}

+ 33 - 0
lib/ui/components/models/custom_button_style.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class CustomButtonStyle {
+  Color defaultButtonColor;
+  Color? pressedButtonColor;
+  Color? disabledButtonColor;
+  Color? defaultBorderColor;
+  Color? pressedBorderColor;
+  Color? disabledBorderColor;
+  Color defaultIconColor;
+  Color? pressedIconColor;
+  Color? disabledIconColor;
+  TextStyle defaultLabelStyle;
+  TextStyle? pressedLabelStyle;
+  TextStyle? disabledLabelStyle;
+  Color? checkIconColor;
+
+  CustomButtonStyle({
+    required this.defaultButtonColor,
+    this.pressedButtonColor,
+    this.disabledButtonColor,
+    this.defaultBorderColor,
+    this.pressedBorderColor,
+    this.disabledBorderColor,
+    required this.defaultIconColor,
+    this.pressedIconColor,
+    this.disabledIconColor,
+    required this.defaultLabelStyle,
+    this.pressedLabelStyle,
+    this.disabledLabelStyle,
+    this.checkIconColor,
+  });
+}

+ 6 - 1
lib/ui/create_collection_page.dart

@@ -196,7 +196,12 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
 
 
   Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
   Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
     final List<CollectionWithThumbnail> collectionsWithThumbnail =
     final List<CollectionWithThumbnail> collectionsWithThumbnail =
-        await CollectionsService.instance.getCollectionsWithThumbnails();
+        await CollectionsService.instance.getCollectionsWithThumbnails(
+      // in collections where user is a collaborator, only addTo and remove
+      // action can to be performed
+      includeCollabCollections:
+          widget.actionType == CollectionActionType.addFiles,
+    );
     collectionsWithThumbnail.sort((first, second) {
     collectionsWithThumbnail.sort((first, second) {
       return compareAsciiLowerCaseNatural(
       return compareAsciiLowerCaseNatural(
         first.collection.name ?? "",
         first.collection.name ?? "",

+ 0 - 3
lib/ui/extents_page_view.dart

@@ -3,8 +3,6 @@
 import 'package:flutter/gestures.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart' hide PageView;
 import 'package:flutter/widgets.dart' hide PageView;
-import 'package:photos/core/event_bus.dart';
-import 'package:photos/events/opened_settings_event.dart';
 
 
 /// This is copy-pasted from the Flutter framework with a support added for building
 /// This is copy-pasted from the Flutter framework with a support added for building
 /// pages off screen using [Viewport.cacheExtents] and a [LayoutBuilder]
 /// pages off screen using [Viewport.cacheExtents] and a [LayoutBuilder]
@@ -295,7 +293,6 @@ class _PageViewState extends State<ExtentsPageView> {
         ? widget.controller.addListener(() {
         ? widget.controller.addListener(() {
             if (widget.controller.offset < -45) {
             if (widget.controller.offset < -45) {
               widget.openDrawer();
               widget.openDrawer();
-              Bus.instance.fire(OpenedSettingsEvent());
             }
             }
           })
           })
         : null;
         : null;

+ 9 - 1
lib/ui/home/home_gallery_widget.dart

@@ -8,9 +8,11 @@ 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/file_load_result.dart';
 import 'package:photos/models/file_load_result.dart';
+import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/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/ignored_files_service.dart';
+import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 
 
 class HomeGalleryWidget extends StatelessWidget {
 class HomeGalleryWidget extends StatelessWidget {
@@ -83,6 +85,12 @@ class HomeGalleryWidget extends StatelessWidget {
       // scrollSafe area -> SafeArea + Preserver more + Nav Bar buttons
       // scrollSafe area -> SafeArea + Preserver more + Nav Bar buttons
       scrollBottomSafeArea: bottomSafeArea + 180,
       scrollBottomSafeArea: bottomSafeArea + 180,
     );
     );
-    return gallery;
+    return Stack(
+      children: [
+        gallery,
+        FileSelectionOverlayBar(GalleryType.homepage, selectedFiles)
+      ],
+    );
+    // return gallery;
   }
   }
 }
 }

+ 1 - 1
lib/ui/home/landing_page_widget.dart

@@ -141,7 +141,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
             "everywhere",
             "everywhere",
             Platform.isAndroid
             Platform.isAndroid
                 ? "Android, iOS, Web, Desktop"
                 ? "Android, iOS, Web, Desktop"
-                : "iOS, Android, Web, Desktop",
+                : "Mobile, Web, Desktop",
           ),
           ),
         ],
         ],
         onPageChanged: (index) {
         onPageChanged: (index) {

+ 2 - 7
lib/ui/home_widget.dart

@@ -20,7 +20,6 @@ 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/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/local_sync_service.dart';
 import 'package:photos/services/local_sync_service.dart';
@@ -47,7 +46,6 @@ import 'package:photos/ui/notification/update/change_log_page.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/viewer/gallery/gallery_overlay_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_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';
@@ -69,6 +67,7 @@ class _HomeWidgetState extends State<HomeWidget> {
 
 
   final _logger = Logger("HomeWidgetState");
   final _logger = Logger("HomeWidgetState");
   final _selectedFiles = SelectedFiles();
   final _selectedFiles = SelectedFiles();
+  final GlobalKey shareButtonKey = GlobalKey();
 
 
   final PageController _pageController = PageController();
   final PageController _pageController = PageController();
   int _selectedTabIndex = 0;
   int _selectedTabIndex = 0;
@@ -378,10 +377,6 @@ class _HomeWidgetState extends State<HomeWidget> {
             ),
             ),
           ),
           ),
         ),
         ),
-        Align(
-          alignment: Alignment.bottomCenter,
-          child: GalleryOverlayWidget(GalleryType.homepage, _selectedFiles),
-        ),
       ],
       ],
     );
     );
   }
   }
@@ -450,7 +445,7 @@ class _HomeWidgetState extends State<HomeWidget> {
       ),
       ),
       backgroundColor: colorScheme.backgroundElevated,
       backgroundColor: colorScheme.backgroundElevated,
       enableDrag: false,
       enableDrag: false,
-      barrierColor: backdropMutedDark,
+      barrierColor: backdropFaintDark,
       context: context,
       context: context,
       builder: (BuildContext context) {
       builder: (BuildContext context) {
         return Padding(
         return Padding(

+ 26 - 7
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -7,10 +7,12 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/clear_selections_event.dart';
 import 'package:photos/events/clear_selections_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/extensions/string_ext.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
@@ -53,7 +55,6 @@ class LazyLoadingGallery extends StatefulWidget {
 }
 }
 
 
 class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
 class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
-  static const kSubGalleryItemLimit = 80;
   static const kRecycleLimit = 400;
   static const kRecycleLimit = 400;
   static const kNumberOfDaysToRenderBeforeAndAfter = 8;
   static const kNumberOfDaysToRenderBeforeAndAfter = 8;
 
 
@@ -243,13 +244,16 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
 
 
   Widget _getGallery() {
   Widget _getGallery() {
     final List<Widget> childGalleries = [];
     final List<Widget> childGalleries = [];
-    for (int index = 0; index < _files.length; index += kSubGalleryItemLimit) {
+    final subGalleryItemLimit = widget.photoGirdSize < photoGridSizeDefault
+        ? subGalleryLimitMin
+        : subGalleryLimitDefault;
+    for (int index = 0; index < _files.length; index += subGalleryItemLimit) {
       childGalleries.add(
       childGalleries.add(
         LazyLoadingGridView(
         LazyLoadingGridView(
           widget.tag,
           widget.tag,
           _files.sublist(
           _files.sublist(
             index,
             index,
-            min(index + kSubGalleryItemLimit, _files.length),
+            min(index + subGalleryItemLimit, _files.length),
           ),
           ),
           widget.asyncLoader,
           widget.asyncLoader,
           widget.selectedFiles,
           widget.selectedFiles,
@@ -306,11 +310,13 @@ class LazyLoadingGridView extends StatefulWidget {
 
 
 class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
 class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
   bool _shouldRender;
   bool _shouldRender;
+  int _currentUserID;
   StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
   StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
 
 
   @override
   @override
   void initState() {
   void initState() {
     _shouldRender = widget.shouldRender;
     _shouldRender = widget.shouldRender;
+    _currentUserID = Configuration.instance.getUserID();
     widget.selectedFiles.addListener(_selectedFilesListener);
     widget.selectedFiles.addListener(_selectedFilesListener);
     _clearSelectionsEvent =
     _clearSelectionsEvent =
         Bus.instance.on<ClearSelectionsEvent>().listen((event) {
         Bus.instance.on<ClearSelectionsEvent>().listen((event) {
@@ -403,6 +409,18 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
   }
   }
 
 
   Widget _buildFile(BuildContext context, File file) {
   Widget _buildFile(BuildContext context, File file) {
+    final isFileSelected = widget.selectedFiles.isFileSelected(file);
+    Color selectionColor = Colors.white;
+    if (isFileSelected &&
+        file.isUploaded &&
+        (file.ownerID != _currentUserID ||
+            file.pubMagicMetadata.uploaderName != null)) {
+      final avatarColors = getEnteColorScheme(context).avatarColors;
+      final int randomID = file.ownerID != _currentUserID
+          ? file.ownerID
+          : file.pubMagicMetadata.uploaderName.sumAsciiValues;
+      selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
+    }
     return GestureDetector(
     return GestureDetector(
       onTap: () {
       onTap: () {
         if (widget.selectedFiles.files.isNotEmpty) {
         if (widget.selectedFiles.files.isNotEmpty) {
@@ -424,7 +442,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
               child: ColorFiltered(
               child: ColorFiltered(
                 colorFilter: ColorFilter.mode(
                 colorFilter: ColorFilter.mode(
                   Colors.black.withOpacity(
                   Colors.black.withOpacity(
-                    widget.selectedFiles.isFileSelected(file) ? 0.4 : 0,
+                    isFileSelected ? 0.4 : 0,
                   ),
                   ),
                   BlendMode.darken,
                   BlendMode.darken,
                 ),
                 ),
@@ -437,18 +455,19 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
                   thumbnailSize: widget.photoGridSize < photoGridSizeDefault
                   thumbnailSize: widget.photoGridSize < photoGridSizeDefault
                       ? thumbnailLargeSize
                       ? thumbnailLargeSize
                       : thumbnailSmallSize,
                       : thumbnailSmallSize,
+                  shouldShowOwnerAvatar: !isFileSelected,
                 ),
                 ),
               ),
               ),
             ),
             ),
             Visibility(
             Visibility(
-              visible: widget.selectedFiles.isFileSelected(file),
-              child: const Positioned(
+              visible: isFileSelected,
+              child: Positioned(
                 right: 4,
                 right: 4,
                 top: 4,
                 top: 4,
                 child: Icon(
                 child: Icon(
                   Icons.check_circle_rounded,
                   Icons.check_circle_rounded,
                   size: 20,
                   size: 20,
-                  color: Colors.white, //same for both themes
+                  color: selectionColor, //same for both themes
                 ),
                 ),
               ),
               ),
             )
             )

+ 17 - 15
lib/ui/huge_listview/scroll_bar_thumb.dart

@@ -31,21 +31,23 @@ class ScrollBarThumb extends StatelessWidget {
     return Row(
     return Row(
       mainAxisAlignment: MainAxisAlignment.end,
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
       children: [
-        FadeTransition(
-          opacity: labelAnimation,
-          child: Container(
-            padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
-            decoration: BoxDecoration(
-              borderRadius: BorderRadius.circular(10),
-              color: backgroundColor,
-            ),
-            child: Text(
-              title,
-              style: TextStyle(
-                color: drawColor,
-                fontWeight: FontWeight.bold,
-                backgroundColor: Colors.transparent,
-                fontSize: 14,
+        IgnorePointer(
+          child: FadeTransition(
+            opacity: labelAnimation,
+            child: Container(
+              padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
+              decoration: BoxDecoration(
+                borderRadius: BorderRadius.circular(10),
+                color: backgroundColor,
+              ),
+              child: Text(
+                title,
+                style: TextStyle(
+                  color: drawColor,
+                  fontWeight: FontWeight.bold,
+                  backgroundColor: Colors.transparent,
+                  fontSize: 14,
+                ),
               ),
               ),
             ),
             ),
           ),
           ),

+ 1 - 1
lib/ui/notification/prompts/password_reminder.dart

@@ -186,7 +186,7 @@ class _PasswordReminderState extends State<PasswordReminder> {
           content: content,
           content: content,
         );
         );
       },
       },
-      barrierColor: enteColor.backdropBaseMute,
+      barrierColor: enteColor.backdropFaint,
     );
     );
   }
   }
 
 

+ 50 - 82
lib/ui/notification/update/change_log_page.dart

@@ -1,16 +1,13 @@
-import 'dart:io';
-import 'dart:ui';
-
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
-import 'package:flutter/gestures.dart';
 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/theme/ente_theme.dart';
-import 'package:photos/ui/common/gradient_button.dart';
-import 'package:photos/ui/common/web_page.dart';
+import 'package:photos/ui/components/button_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/notification/update/change_log_entry.dart';
 import 'package:photos/ui/notification/update/change_log_entry.dart';
+import 'package:url_launcher/url_launcher_string.dart';
 
 
 class ChangeLogPage extends StatefulWidget {
 class ChangeLogPage extends StatefulWidget {
   const ChangeLogPage({
   const ChangeLogPage({
@@ -30,7 +27,6 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final enteColorScheme = getEnteColorScheme(context);
     final enteColorScheme = getEnteColorScheme(context);
-    final enteTextTheme = getEnteTextTheme(context);
     return Scaffold(
     return Scaffold(
       appBar: null,
       appBar: null,
       body: Container(
       body: Container(
@@ -68,57 +64,31 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
                 child: Column(
                 child: Column(
                   crossAxisAlignment: CrossAxisAlignment.start,
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
                   children: [
-                    SizedBox(
-                      width: double.infinity,
-                      child: GradientButton(
-                        onTap: () async {
-                          await UpdateService.instance.hideChangeLog();
-                          if (mounted && Navigator.of(context).canPop()) {
-                            Navigator.of(context).pop();
-                          }
-                        },
-                        text: "Let's go",
-                      ),
+                    ButtonWidget(
+                      buttonType: ButtonType.trailingIconPrimary,
+                      buttonSize: ButtonSize.large,
+                      labelText: "Continue",
+                      icon: Icons.arrow_forward_outlined,
+                      onTap: () async {
+                        await UpdateService.instance.hideChangeLog();
+                        if (mounted && Navigator.of(context).canPop()) {
+                          Navigator.of(context).pop();
+                        }
+                      },
+                    ),
+                    const SizedBox(
+                      height: 8,
                     ),
                     ),
-                    Padding(
-                      padding: const EdgeInsets.only(
-                        left: 12,
-                        top: 12,
-                        right: 12,
-                        bottom: 6,
-                      ),
-                      child: RichText(
-                        textAlign: TextAlign.center,
-                        text: TextSpan(
-                          children: [
-                            const TextSpan(
-                              text: "If you like ente, ",
-                            ),
-                            TextSpan(
-                              text: "let others know",
-                              style: enteTextTheme.small.copyWith(
-                                color: enteColorScheme.primary700,
-                                decoration: TextDecoration.underline,
-                              ),
-                              recognizer: TapGestureRecognizer()
-                                ..onTap = () {
-                                  // Single tapped.
-                                  Navigator.of(context).push(
-                                    MaterialPageRoute(
-                                      builder: (BuildContext context) {
-                                        return const WebPage(
-                                          "Spread the word",
-                                          "https://ente.io/share/",
-                                        );
-                                      },
-                                    ),
-                                  );
-                                },
-                            ),
-                          ],
-                          style: enteTextTheme.small,
-                        ),
-                      ),
+                    ButtonWidget(
+                      buttonType: ButtonType.trailingIconSecondary,
+                      buttonSize: ButtonSize.large,
+                      labelText: "Rate the app",
+                      icon: Icons.favorite_rounded,
+                      iconColor: enteColorScheme.primary500,
+                      onTap: () async {
+                        launchUrlString(
+                            UpdateService.instance.getRateDetails().item2);
+                      },
                     ),
                     ),
                     const SizedBox(height: 8),
                     const SizedBox(height: 8),
                   ],
                   ],
@@ -136,47 +106,45 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
     final List<ChangeLogEntry> items = [];
     final List<ChangeLogEntry> items = [];
     items.add(
     items.add(
       ChangeLogEntry(
       ChangeLogEntry(
-        "Select all photos in a day",
-        "After you select a photo, you'll now see an option next to the date to select all photos from that day.",
+        "Collect photos from anyone!",
+        "You can now enable \"Allow adding photos\" under shared link "
+            "settings to allow anyone with access to the link to also add "
+            "photos to that shared album.\n\nThis is the perfect fit for "
+            "occasions where you want to ask all your friends and relatives who attended the event to add the photos they took to an album. You can then prune them there; plus everyone can view them in a single place.",
       ),
       ),
     );
     );
     items.add(
     items.add(
       ChangeLogEntry(
       ChangeLogEntry(
-        '''Easier access to favorites''',
-        "Your favorites now have a special heart icon, and will appear first in the list of albums. Archived albums also get a new indicator.",
+        '''Customize photo grid size''',
+        "You can now change the number of photos that are shown in a row."
+            "\n\nSince this was a much requested feature we've released it as "
+            "an option in Settings > General > Advanced; later we'll also try a gesture for easier access.",
       ),
       ),
     );
     );
 
 
     items.add(
     items.add(
       ChangeLogEntry(
       ChangeLogEntry(
-        '''Export photo descriptions''',
-        "When you export data out of ente using the desktop app, any photo captions and descriptions that you added will also be exported.",
+        '''Better multi-select, and hide''',
+        "The item selector gets a new, expanded look with clearly marked "
+            "actions. We'll use this revamped space to show even more actions"
+            " you can take on selected photos.\n\nAnd we've already added new "
+            "actions! You can now select multiple items and hide all of them in one go.",
       ),
       ),
     );
     );
     items.add(
     items.add(
       ChangeLogEntry(
       ChangeLogEntry(
-        '''Initial support for empty albums''',
-        "Any empty albums that you already have will now show up in ente. You can choose to delete them, or add more photos to them. In the future we'll support more workflows with empty albums.",
+        '''Per album free up space''',
+        "There is now an option to free up space within each on device album. This provides both a more granular, and faster, way to save storage your phone.",
+      ),
+    );
+
+    items.add(
+      ChangeLogEntry(
+        '''Longer photo descriptions''',
+        "The previous 280 character limit on photo captions and descriptions has been increased to 5000.",
         isFeature: false,
         isFeature: false,
       ),
       ),
     );
     );
-    if (Platform.isIOS) {
-      items.add(
-        ChangeLogEntry(
-          '''Tweak video uploads''',
-          "ente will now keep videos temporarily cached until they get successfully uploaded. This will make video uploads work better as long as the app is not force killed.",
-          isFeature: false,
-        ),
-      );
-    } else {
-      items.add(
-        ChangeLogEntry(
-          '''Better timestamps for screenshots''',
-          "Added more cases when deducing photo dates from their file names. ente will also automatically apply these rules to fix photos that have already been imported without a valid date.",
-          isFeature: false,
-        ),
-      );
-    }
 
 
     return Container(
     return Container(
       padding: const EdgeInsets.only(left: 16),
       padding: const EdgeInsets.only(left: 16),

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

@@ -9,6 +9,7 @@ import 'package:photos/models/subscription.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/services/billing_service.dart';
 import 'package:photos/services/billing_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/common/bottom_shadow.dart';
 import 'package:photos/ui/common/bottom_shadow.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
@@ -499,6 +500,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
             value: _showYearlyPlan,
             value: _showYearlyPlan,
             activeColor: Colors.white,
             activeColor: Colors.white,
             inactiveThumbColor: Colors.white,
             inactiveThumbColor: Colors.white,
+            activeTrackColor: getEnteColorScheme(context).strokeMuted,
             onChanged: (value) async {
             onChanged: (value) async {
               _showYearlyPlan = value;
               _showYearlyPlan = value;
               await _filterStripeForUI();
               await _filterStripeForUI();

+ 1 - 1
lib/ui/settings/about_section_widget.dart

@@ -37,7 +37,7 @@ class AboutSectionWidget extends StatelessWidget {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
-            launchUrl(Uri.parse("https://github.com/ente-io/frame"));
+            launchUrl(Uri.parse("https://github.com/ente-io/photos-app"));
           },
           },
         ),
         ),
         sectionOptionSpacing,
         sectionOptionSpacing,

+ 1 - 1
lib/ui/settings/common_settings.dart

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 
 
 Widget sectionOptionSpacing = const SizedBox(height: 6);
 Widget sectionOptionSpacing = const SizedBox(height: 6);
 
 
-ExpandableThemeData getExpandableTheme(BuildContext context) {
+ExpandableThemeData getExpandableTheme() {
   return const ExpandableThemeData(
   return const ExpandableThemeData(
     hasIcon: false,
     hasIcon: false,
     useInkWell: false,
     useInkWell: false,

+ 5 - 23
lib/ui/settings/settings_title_bar_widget.dart

@@ -12,6 +12,7 @@ class SettingsTitleBarWidget extends StatelessWidget {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final logger = Logger((SettingsTitleBarWidget).toString());
     final logger = Logger((SettingsTitleBarWidget).toString());
+    final userDetails = InheritedUserDetails.of(context)?.userDetails;
     return Container(
     return Container(
       padding: const EdgeInsets.symmetric(vertical: 4),
       padding: const EdgeInsets.symmetric(vertical: 4),
       child: Padding(
       child: Padding(
@@ -26,31 +27,12 @@ class SettingsTitleBarWidget extends StatelessWidget {
               },
               },
               icon: const Icon(Icons.keyboard_double_arrow_left_outlined),
               icon: const Icon(Icons.keyboard_double_arrow_left_outlined),
             ),
             ),
-            FutureBuilder(
-              future: InheritedUserDetails.of(context)?.userDetails,
-              builder: (context, snapshot) {
-                if (InheritedUserDetails.of(context) == null) {
-                  logger.severe(
-                    (InheritedUserDetails).toString() +
-                        ' not found before ' +
-                        (SettingsTitleBarWidget).toString() +
-                        ' on tree',
-                  );
-                  throw Error();
-                } else if (snapshot.hasData) {
-                  final userDetails = snapshot.data as UserDetails;
-                  return Text(
+            userDetails is UserDetails
+                ? Text(
                     "${NumberFormat().format(userDetails.fileCount)} memories",
                     "${NumberFormat().format(userDetails.fileCount)} memories",
                     style: getEnteTextTheme(context).largeBold,
                     style: getEnteTextTheme(context).largeBold,
-                  );
-                } else if (snapshot.hasError) {
-                  logger.severe('failed to load user details');
-                  return const EnteLoadingWidget();
-                } else {
-                  return const EnteLoadingWidget();
-                }
-              },
-            )
+                  )
+                : const EnteLoadingWidget(),
           ],
           ],
         ),
         ),
       ),
       ),

+ 6 - 8
lib/ui/settings/social_section_widget.dart

@@ -26,14 +26,12 @@ class SocialSectionWidget extends StatelessWidget {
     final result = UpdateService.instance.getRateDetails();
     final result = UpdateService.instance.getRateDetails();
     final String ratePlace = result.item1;
     final String ratePlace = result.item1;
     final String rateUrl = result.item2;
     final String rateUrl = result.item2;
-    if (!UpdateService.instance.isIndependent()) {
-      options.addAll(
-        [
-          SocialsMenuItemWidget("Rate us on $ratePlace", rateUrl),
-          sectionOptionSpacing,
-        ],
-      );
-    }
+    options.addAll(
+      [
+        SocialsMenuItemWidget("Rate us on $ratePlace", rateUrl),
+        sectionOptionSpacing,
+      ],
+    );
     options.addAll(
     options.addAll(
       [
       [
         sectionOptionSpacing,
         sectionOptionSpacing,

+ 12 - 21
lib/ui/settings/storage_card_widget.dart

@@ -2,6 +2,7 @@ import 'dart:math';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/models/user_details.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/colors.dart';
@@ -9,7 +10,6 @@ import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 // ignore: import_of_legacy_library_into_null_safe
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/ui/payment/subscription.dart';
 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/ui/settings/storage_progress_widget.dart';
 import 'package:photos/utils/data_util.dart';
 import 'package:photos/utils/data_util.dart';
 
 
@@ -51,6 +51,7 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final inheritedUserDetails = InheritedUserDetails.of(context);
     final inheritedUserDetails = InheritedUserDetails.of(context);
+    final userDetails = inheritedUserDetails?.userDetails;
 
 
     if (inheritedUserDetails == null) {
     if (inheritedUserDetails == null) {
       _logger.severe(
       _logger.severe(
@@ -72,13 +73,13 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
         onTapDown: (details) => _isStorageCardPressed.value = true,
         onTapDown: (details) => _isStorageCardPressed.value = true,
         onTapCancel: () => _isStorageCardPressed.value = false,
         onTapCancel: () => _isStorageCardPressed.value = false,
         onTapUp: (details) => _isStorageCardPressed.value = false,
         onTapUp: (details) => _isStorageCardPressed.value = false,
-        child: containerForUserDetails(inheritedUserDetails),
+        child: containerForUserDetails(userDetails),
       );
       );
     }
     }
   }
   }
 
 
   Widget containerForUserDetails(
   Widget containerForUserDetails(
-    InheritedUserDetails inheritedUserDetails,
+    UserDetails? userDetails,
   ) {
   ) {
     return ConstrainedBox(
     return ConstrainedBox(
       constraints: const BoxConstraints(maxWidth: 350),
       constraints: const BoxConstraints(maxWidth: 350),
@@ -87,22 +88,11 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
         child: Stack(
         child: Stack(
           children: [
           children: [
             _background,
             _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);
-              },
-            ),
+            userDetails is UserDetails
+                ? _userDetails(userDetails)
+                : const EnteLoadingWidget(
+                    color: strokeBaseDark,
+                  ),
             Align(
             Align(
               alignment: Alignment.centerRight,
               alignment: Alignment.centerRight,
               child: Padding(
               child: Padding(
@@ -124,7 +114,7 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
     );
     );
   }
   }
 
 
-  Widget userDetails(UserDetails userDetails) {
+  Widget _userDetails(UserDetails userDetails) {
     const hundredMBinBytes = 107374182;
     const hundredMBinBytes = 107374182;
     const oneTBinBytes = 1073741824000;
     const oneTBinBytes = 1073741824000;
 
 
@@ -132,7 +122,8 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
     final totalStorageInBytes = userDetails.getTotalStorage();
     final totalStorageInBytes = userDetails.getTotalStorage();
     final freeStorageInBytes = totalStorageInBytes - usedStorageInBytes;
     final freeStorageInBytes = totalStorageInBytes - usedStorageInBytes;
 
 
-    final isMobileScreenSmall = MediaQuery.of(context).size.width <= 336;
+    final isMobileScreenSmall =
+        MediaQuery.of(context).size.width <= mobileSmallThreshold;
     final shouldShowFreeSpaceInMBs = freeStorageInBytes < hundredMBinBytes;
     final shouldShowFreeSpaceInMBs = freeStorageInBytes < hundredMBinBytes;
     final shouldShowFreeSpaceInTBs = freeStorageInBytes >= oneTBinBytes;
     final shouldShowFreeSpaceInTBs = freeStorageInBytes >= oneTBinBytes;
     final shouldShowUsedStorageInTBs = usedStorageInBytes >= oneTBinBytes;
     final shouldShowUsedStorageInTBs = usedStorageInBytes >= oneTBinBytes;

+ 4 - 1
lib/ui/settings_page.dart

@@ -5,6 +5,8 @@ import 'dart:io';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 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/event_bus.dart';
+import 'package:photos/events/opened_settings_event.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/settings/about_section_widget.dart';
 import 'package:photos/ui/settings/about_section_widget.dart';
@@ -26,10 +28,11 @@ class SettingsPage extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
+    Bus.instance.fire(OpenedSettingsEvent());
     final enteColorScheme = getEnteColorScheme(context);
     final enteColorScheme = getEnteColorScheme(context);
     return Scaffold(
     return Scaffold(
       body: Container(
       body: Container(
-        color: enteColorScheme.backdropBase,
+        color: enteColorScheme.backdropMuted,
         child: _getBody(context, enteColorScheme),
         child: _getBody(context, enteColorScheme),
       ),
       ),
     );
     );

+ 13 - 17
lib/ui/shared_collections_gallery.dart

@@ -9,7 +9,6 @@ import 'package:logging/logging.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/db/files_db.dart';
-import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
@@ -17,9 +16,11 @@ import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/colors.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/sharing/user_avator_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 import 'package:photos/ui/viewer/gallery/collection_page.dart';
 import 'package:photos/ui/viewer/gallery/collection_page.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/navigation_util.dart';
@@ -317,7 +318,12 @@ class OutgoingCollectionItem extends StatelessWidget {
                       const Padding(padding: EdgeInsets.all(2)),
                       const Padding(padding: EdgeInsets.all(2)),
                       c.collection.publicURLs.isEmpty
                       c.collection.publicURLs.isEmpty
                           ? Container()
                           ? Container()
-                          : const Icon(Icons.link),
+                          : (c.collection.publicURLs.first.isExpired
+                              ? const Icon(
+                                  Icons.link,
+                                  color: warning500,
+                                )
+                              : const Icon(Icons.link)),
                     ],
                     ],
                   ),
                   ),
                   sharees.isEmpty
                   sharees.isEmpty
@@ -392,21 +398,11 @@ class IncomingCollectionItem extends StatelessWidget {
                   ),
                   ),
                   Align(
                   Align(
                     alignment: Alignment.bottomRight,
                     alignment: Alignment.bottomRight,
-                    child: Container(
-                      padding: const EdgeInsets.all(8),
-                      margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
-                      decoration: BoxDecoration(
-                        shape: BoxShape.circle,
-                        color: Theme.of(context)
-                            .colorScheme
-                            .defaultBackgroundColor,
-                      ),
-                      child: Text(
-                        c.collection.owner.name == null ||
-                                c.collection.owner.name.isEmpty
-                            ? c.collection.owner.email.substring(0, 1)
-                            : c.collection.owner.name.substring(0, 1),
-                        textAlign: TextAlign.center,
+                    child: Padding(
+                      padding: const EdgeInsets.only(right: 8.0, bottom: 8.0),
+                      child: UserAvatarWidget(
+                        c.collection.owner,
+                        thumbnailView: true,
                       ),
                       ),
                     ),
                     ),
                   ),
                   ),

+ 332 - 0
lib/ui/sharing/add_partipant_page.dart

@@ -0,0 +1,332 @@
+import 'package:email_validator/email_validator.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
+import 'package:photos/ui/common/gradient_button.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_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/menu_section_title.dart';
+import 'package:photos/ui/sharing/user_avator_widget.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class AddParticipantPage extends StatefulWidget {
+  final Collection collection;
+
+  const AddParticipantPage(this.collection, {super.key});
+
+  @override
+  State<StatefulWidget> createState() => _AddParticipantPage();
+}
+
+class _AddParticipantPage extends State<AddParticipantPage> {
+  late bool selectAsViewer;
+  String selectedEmail = '';
+  String _email = '';
+  bool hideListOfEmails = false;
+  bool _emailIsValid = false;
+  bool isKeypadOpen = false;
+  late CollectionActions collectionActions;
+
+  // Focus nodes are necessary
+  final textFieldFocusNode = FocusNode();
+  final _textController = TextEditingController();
+
+  @override
+  void initState() {
+    selectAsViewer = true;
+    collectionActions = CollectionActions(CollectionsService.instance);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _textController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
+    final enteTextTheme = getEnteTextTheme(context);
+    final List<User> suggestedUsers = _getSuggestedUser();
+    hideListOfEmails = suggestedUsers.isEmpty;
+    return Scaffold(
+      resizeToAvoidBottomInset: isKeypadOpen,
+      appBar: AppBar(
+        title: const Text("Add people"),
+      ),
+      body: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          const SizedBox(height: 12),
+          Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 16.0),
+            child: Text(
+              "Add a new email",
+              style: enteTextTheme.body,
+            ),
+          ),
+          const SizedBox(height: 4),
+          Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 16.0),
+            child: _getEmailField(),
+          ),
+          (hideListOfEmails || isKeypadOpen)
+              ? const Expanded(child: SizedBox())
+              : Expanded(
+                  child: Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
+                    child: Column(
+                      children: [
+                        const SizedBox(height: 24),
+                        const MenuSectionTitle(
+                          title: "or pick an existing one",
+                        ),
+                        Expanded(
+                          child: ListView.builder(
+                            itemBuilder: (context, index) {
+                              final currentUser = suggestedUsers[index];
+                              return Column(
+                                children: [
+                                  MenuItemWidget(
+                                    captionedTextWidget: CaptionedTextWidget(
+                                      title: currentUser.email,
+                                    ),
+                                    leadingIconSize: 24.0,
+                                    leadingIconWidget: UserAvatarWidget(
+                                      currentUser,
+                                      type: AvatarType.mini,
+                                    ),
+                                    menuItemColor:
+                                        getEnteColorScheme(context).fillFaint,
+                                    pressedColor:
+                                        getEnteColorScheme(context).fillFaint,
+                                    trailingIcon:
+                                        (selectedEmail == currentUser.email)
+                                            ? Icons.check
+                                            : null,
+                                    onTap: () async {
+                                      textFieldFocusNode.unfocus();
+                                      if (selectedEmail == currentUser.email) {
+                                        selectedEmail = '';
+                                      } else {
+                                        selectedEmail = currentUser.email;
+                                      }
+
+                                      setState(() => {});
+                                      // showShortToast(context, "yet to implement");
+                                    },
+                                    isTopBorderRadiusRemoved: index > 0,
+                                    isBottomBorderRadiusRemoved:
+                                        index < (suggestedUsers.length - 1),
+                                  ),
+                                  (index == (suggestedUsers.length - 1))
+                                      ? const SizedBox.shrink()
+                                      : DividerWidget(
+                                          dividerType: DividerType.menu,
+                                          bgColor: getEnteColorScheme(context)
+                                              .fillFaint,
+                                        ),
+                                ],
+                              );
+                            },
+                            itemCount: suggestedUsers.length,
+
+                            // physics: const ClampingScrollPhysics(),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ),
+          const DividerWidget(
+            dividerType: DividerType.solid,
+          ),
+          SafeArea(
+            child: Padding(
+              padding: const EdgeInsets.only(
+                top: 8,
+                bottom: 8,
+                left: 16,
+                right: 16,
+              ),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  const MenuSectionTitle(title: "Add as"),
+                  MenuItemWidget(
+                    captionedTextWidget: const CaptionedTextWidget(
+                      title: "Collaborator",
+                    ),
+                    leadingIcon: Icons.edit_outlined,
+                    menuItemColor: getEnteColorScheme(context).fillFaint,
+                    pressedColor: getEnteColorScheme(context).fillFaint,
+                    trailingIcon: !selectAsViewer ? Icons.check : null,
+                    onTap: () async {
+                      if (kDebugMode) {
+                        setState(() => {selectAsViewer = false});
+                      } else {
+                        showShortToast(context, "Coming soon...");
+                      }
+                    },
+                    isBottomBorderRadiusRemoved: true,
+                  ),
+                  DividerWidget(
+                    dividerType: DividerType.menu,
+                    bgColor: getEnteColorScheme(context).fillFaint,
+                  ),
+                  MenuItemWidget(
+                    captionedTextWidget: const CaptionedTextWidget(
+                      title: "Viewer",
+                    ),
+                    leadingIcon: Icons.photo_outlined,
+                    menuItemColor: getEnteColorScheme(context).fillFaint,
+                    pressedColor: getEnteColorScheme(context).fillFaint,
+                    trailingIcon: selectAsViewer ? Icons.check : null,
+                    onTap: () async {
+                      setState(() => {selectAsViewer = true});
+                      // showShortToast(context, "yet to implement");
+                    },
+                    isTopBorderRadiusRemoved: true,
+                  ),
+                  !isKeypadOpen
+                      ? const MenuSectionDescriptionWidget(
+                          content:
+                              "Collaborators can add photos and videos to the shared album.",
+                        )
+                      : const SizedBox.shrink(),
+                  const SizedBox(height: 12),
+                  SizedBox(
+                    width: double.infinity,
+                    child: GradientButton(
+                      onTap: (selectedEmail == '' && !_emailIsValid)
+                          ? null
+                          : () async {
+                              final emailToAdd =
+                                  selectedEmail == '' ? _email : selectedEmail;
+                              final result =
+                                  await collectionActions.addEmailToCollection(
+                                context,
+                                widget.collection,
+                                emailToAdd,
+                                role: selectAsViewer
+                                    ? CollectionParticipantRole.viewer
+                                    : CollectionParticipantRole.collaborator,
+                              );
+                              if (result != null && result && mounted) {
+                                Navigator.of(context).pop(true);
+                              }
+                            },
+                      text: selectAsViewer ? "Add viewer" : "Add collaborator",
+                    ),
+                  ),
+                  const SizedBox(height: 8),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  void clearFocus() {
+    _textController.clear();
+    _email = _textController.text;
+    _emailIsValid = false;
+    textFieldFocusNode.unfocus();
+    setState(() => {});
+  }
+
+  Widget _getEmailField() {
+    return TextFormField(
+      controller: _textController,
+      focusNode: textFieldFocusNode,
+      style: getEnteTextTheme(context).body,
+      autofillHints: const [AutofillHints.email],
+      decoration: InputDecoration(
+        focusedBorder: OutlineInputBorder(
+          borderRadius: const BorderRadius.all(Radius.circular(4.0)),
+          borderSide:
+              BorderSide(color: getEnteColorScheme(context).strokeMuted),
+        ),
+        fillColor: getEnteColorScheme(context).fillFaint,
+        filled: true,
+        hintText: 'Enter email',
+        contentPadding: const EdgeInsets.symmetric(
+          horizontal: 16,
+          vertical: 14,
+        ),
+        border: UnderlineInputBorder(
+          borderSide: BorderSide.none,
+          borderRadius: BorderRadius.circular(4),
+        ),
+        prefixIcon: Icon(
+          Icons.email_outlined,
+          color: getEnteColorScheme(context).strokeMuted,
+        ),
+        suffixIcon: _email == ''
+            ? null
+            : IconButton(
+                onPressed: clearFocus,
+                icon: Icon(
+                  Icons.cancel,
+                  color: getEnteColorScheme(context).strokeMuted,
+                ),
+              ),
+      ),
+      onChanged: (value) {
+        if (selectedEmail != '') {
+          selectedEmail = '';
+        }
+        _email = value.trim();
+        if (_emailIsValid != EmailValidator.validate(_email)) {
+          setState(() {
+            _emailIsValid = EmailValidator.validate(_email);
+          });
+        } else if (_email.length < 2) {
+          setState(() {});
+        }
+      },
+      autocorrect: false,
+      keyboardType: TextInputType.emailAddress,
+      //initialValue: _email,
+      textInputAction: TextInputAction.next,
+    );
+  }
+
+  List<User> _getSuggestedUser() {
+    final List<User> suggestedUsers = [];
+    final Set<int> existingUserIDs = {};
+    final int ownerID = Configuration.instance.getUserID()!;
+    for (final User? u in widget.collection.sharees ?? []) {
+      if (u != null && u.id != null) {
+        existingUserIDs.add(u.id!);
+      }
+    }
+    for (final c in CollectionsService.instance.getActiveCollections()) {
+      if (c.owner?.id == ownerID) {
+        for (final User? u in c?.sharees ?? []) {
+          if (u != null && u.id != null && !existingUserIDs.contains(u.id)) {
+            existingUserIDs.add(u.id!);
+            suggestedUsers.add(u);
+          }
+        }
+      } else if (c.owner != null &&
+          c.owner!.id != null &&
+          !existingUserIDs.contains(c.owner!.id!)) {
+        existingUserIDs.add(c.owner!.id!);
+        suggestedUsers.add(c.owner!);
+      }
+    }
+    suggestedUsers.sort((a, b) => a.email.compareTo(b.email));
+    return suggestedUsers;
+  }
+}

+ 275 - 0
lib/ui/sharing/album_participants_page.dart

@@ -0,0 +1,275 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/extensions/list.dart';
+import 'package:photos/models/collection.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/menu_item_widget.dart';
+import 'package:photos/ui/components/menu_section_title.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/ui/sharing/add_partipant_page.dart';
+import 'package:photos/ui/sharing/manage_album_participant.dart';
+import 'package:photos/ui/sharing/user_avator_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class AlbumParticipantsPage extends StatefulWidget {
+  final Collection collection;
+
+  const AlbumParticipantsPage(
+    this.collection, {
+    super.key,
+  });
+
+  @override
+  State<AlbumParticipantsPage> createState() => _AlbumParticipantsPageState();
+}
+
+class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
+  late int currentUserID;
+
+  @override
+  void initState() {
+    currentUserID = Configuration.instance.getUserID()!;
+    super.initState();
+  }
+
+  Future<void> _navigateToManageUser(User user) async {
+    if (user.id == currentUserID) {
+      return;
+    }
+    await routeToPage(
+      context,
+      ManageIndividualParticipant(collection: widget.collection, user: user),
+    );
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  Future<void> _navigateToAddUser(bool addingViewer) async {
+    await routeToPage(
+      context,
+      AddParticipantPage(widget.collection),
+    );
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final isOwner =
+        widget.collection.owner?.id == Configuration.instance.getUserID();
+    final colorScheme = getEnteColorScheme(context);
+    final currentUserID = Configuration.instance.getUserID()!;
+    final int participants = 1 + widget.collection.getSharees().length;
+    final User owner = widget.collection.owner!;
+    if (owner.id == currentUserID && owner.email == "") {
+      owner.email = Configuration.instance.getEmail()!;
+    }
+    final splitResult =
+        widget.collection.getSharees().splitMatch((x) => x.isViewer);
+    final List<User> viewers = splitResult.matched;
+    final List<User> collaborators = splitResult.unmatched;
+
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          TitleBarWidget(
+            flexibleSpaceTitle: TitleBarTitleWidget(
+              title: "${widget.collection.name}",
+            ),
+            flexibleSpaceCaption: "$participants Participants",
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.only(top: 20, left: 16, right: 16),
+                  child: Column(
+                    children: [
+                      Column(
+                        children: [
+                          const MenuSectionTitle(
+                            title: "Owner",
+                            iconData: Icons.admin_panel_settings_outlined,
+                          ),
+                          MenuItemWidget(
+                            captionedTextWidget: CaptionedTextWidget(
+                              title: isOwner
+                                  ? "You"
+                                  : widget.collection.owner?.email ?? '',
+                              makeTextBold: isOwner,
+                            ),
+                            leadingIconWidget: UserAvatarWidget(
+                              owner,
+                              currentUserID: currentUserID,
+                            ),
+                            leadingIconSize: 24,
+                            menuItemColor: colorScheme.fillFaint,
+                            borderRadius: 8,
+                            isGestureDetectorDisabled: true,
+                          ),
+                        ],
+                      ),
+                    ],
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+          SliverPadding(
+            padding: const EdgeInsets.only(top: 20, left: 16, right: 16),
+            sliver: SliverList(
+              delegate: SliverChildBuilderDelegate(
+                (context, index) {
+                  if (index == 0 && (isOwner || collaborators.isNotEmpty)) {
+                    return const MenuSectionTitle(
+                      title: "Collaborator",
+                      iconData: Icons.edit_outlined,
+                    );
+                  } else if (index > 0 && index <= collaborators.length) {
+                    final listIndex = index - 1;
+                    final currentUser = collaborators[listIndex];
+                    final isSameAsLoggedInUser =
+                        currentUserID == currentUser.id;
+                    return Column(
+                      children: [
+                        MenuItemWidget(
+                          captionedTextWidget: CaptionedTextWidget(
+                            title: isSameAsLoggedInUser
+                                ? "You"
+                                : currentUser.email,
+                            makeTextBold: isSameAsLoggedInUser,
+                          ),
+                          leadingIconSize: 24.0,
+                          leadingIconWidget: UserAvatarWidget(
+                            currentUser,
+                            type: AvatarType.mini,
+                            currentUserID: currentUserID,
+                          ),
+                          menuItemColor: getEnteColorScheme(context).fillFaint,
+                          pressedColor: getEnteColorScheme(context).fillFaint,
+                          trailingIcon: isOwner ? Icons.chevron_right : null,
+                          trailingIconIsMuted: true,
+                          onTap: () async {
+                            if (isOwner) {
+                              await _navigateToManageUser(currentUser);
+                            }
+                          },
+                          isTopBorderRadiusRemoved: listIndex > 0,
+                          isBottomBorderRadiusRemoved: true,
+                          borderRadius: 8,
+                        ),
+                        DividerWidget(
+                          dividerType: DividerType.menu,
+                          bgColor: getEnteColorScheme(context).blurStrokeFaint,
+                        ),
+                      ],
+                    );
+                  } else if (index == (1 + collaborators.length) && isOwner) {
+                    return MenuItemWidget(
+                      captionedTextWidget: CaptionedTextWidget(
+                        title:
+                            collaborators.isNotEmpty ? "Add more" : "Add email",
+                        makeTextBold: true,
+                      ),
+                      leadingIcon: Icons.add_outlined,
+                      menuItemColor: getEnteColorScheme(context).fillFaint,
+                      pressedColor: getEnteColorScheme(context).fillFaint,
+                      onTap: () async {
+                        await _navigateToAddUser(false);
+                      },
+                      isTopBorderRadiusRemoved: collaborators.isNotEmpty,
+                      borderRadius: 8,
+                    );
+                  }
+                  return const SizedBox.shrink();
+                },
+                childCount: 1 + collaborators.length + 1,
+              ),
+            ),
+          ),
+          SliverPadding(
+            padding: const EdgeInsets.only(top: 24, left: 16, right: 16),
+            sliver: SliverList(
+              delegate: SliverChildBuilderDelegate(
+                (context, index) {
+                  if (index == 0 && (isOwner || viewers.isNotEmpty)) {
+                    return const MenuSectionTitle(
+                      title: "Viewer",
+                      iconData: Icons.photo_outlined,
+                    );
+                  } else if (index > 0 && index <= viewers.length) {
+                    final listIndex = index - 1;
+                    final currentUser = viewers[listIndex];
+                    final isSameAsLoggedInUser =
+                        currentUserID == currentUser.id;
+                    final isLastItem = !isOwner && index == viewers.length;
+                    return Column(
+                      children: [
+                        MenuItemWidget(
+                          captionedTextWidget: CaptionedTextWidget(
+                            title: isSameAsLoggedInUser
+                                ? "You"
+                                : currentUser.email,
+                            makeTextBold: isSameAsLoggedInUser,
+                          ),
+                          leadingIconSize: 24.0,
+                          leadingIconWidget: UserAvatarWidget(
+                            currentUser,
+                            type: AvatarType.mini,
+                            currentUserID: currentUserID,
+                          ),
+                          menuItemColor: getEnteColorScheme(context).fillFaint,
+                          pressedColor: getEnteColorScheme(context).fillFaint,
+                          trailingIcon: isOwner ? Icons.chevron_right : null,
+                          trailingIconIsMuted: true,
+                          onTap: () async {
+                            if (isOwner) {
+                              await _navigateToManageUser(currentUser);
+                            }
+                          },
+                          isTopBorderRadiusRemoved: listIndex > 0,
+                          isBottomBorderRadiusRemoved: !isLastItem,
+                          borderRadius: 8,
+                        ),
+                        isLastItem
+                            ? const SizedBox.shrink()
+                            : DividerWidget(
+                                dividerType: DividerType.menu,
+                                bgColor: getEnteColorScheme(context).fillFaint,
+                              ),
+                      ],
+                    );
+                  } else if (index == (1 + viewers.length) && isOwner) {
+                    return MenuItemWidget(
+                      captionedTextWidget: CaptionedTextWidget(
+                        title: viewers.isNotEmpty ? "Add more" : "Add Viewer",
+                        makeTextBold: true,
+                      ),
+                      leadingIcon: Icons.add_outlined,
+                      menuItemColor: getEnteColorScheme(context).fillFaint,
+                      pressedColor: getEnteColorScheme(context).fillFaint,
+                      onTap: () async {
+                        await _navigateToAddUser(true);
+                      },
+                      isTopBorderRadiusRemoved: viewers.isNotEmpty,
+                      borderRadius: 8,
+                    );
+                  }
+                  return const SizedBox.shrink();
+                },
+                childCount: 1 + viewers.length + 1,
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 163 - 0
lib/ui/sharing/manage_album_participant.dart

@@ -0,0 +1,163 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/models/collection.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/actions/collection/collection_sharing_actions.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_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/menu_section_title.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class ManageIndividualParticipant extends StatefulWidget {
+  final Collection collection;
+  final User user;
+
+  const ManageIndividualParticipant({
+    super.key,
+    required this.collection,
+    required this.user,
+  });
+
+  @override
+  State<StatefulWidget> createState() => _ManageIndividualParticipantState();
+}
+
+class _ManageIndividualParticipantState
+    extends State<ManageIndividualParticipant> {
+  final CollectionActions collectionActions =
+      CollectionActions(CollectionsService.instance);
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    final textTheme = getEnteTextTheme(context);
+    return Scaffold(
+      appBar: AppBar(),
+      body: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 16.0),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            SafeArea(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  const SizedBox(
+                    height: 12,
+                  ),
+                  const TitleBarTitleWidget(
+                    title: "Manage",
+                  ),
+                  Text(
+                    widget.user.email.toString().trim(),
+                    textAlign: TextAlign.left,
+                    style:
+                        textTheme.small.copyWith(color: colorScheme.textMuted),
+                  ),
+                ],
+              ),
+            ),
+            const SizedBox(height: 12),
+            const MenuSectionTitle(title: "Added as"),
+            MenuItemWidget(
+              captionedTextWidget: const CaptionedTextWidget(
+                title: "Collaborator",
+              ),
+              leadingIcon: Icons.edit_outlined,
+              menuItemColor: getEnteColorScheme(context).fillFaint,
+              pressedColor: getEnteColorScheme(context).fillFaint,
+              trailingIcon: widget.user.isCollaborator ? Icons.check : null,
+              onTap: widget.user.isCollaborator
+                  ? null
+                  : () async {
+                      if (!kDebugMode) {
+                        showShortToast(context, "Coming soon...");
+                        return;
+                      }
+                      final result =
+                          await collectionActions.addEmailToCollection(
+                        context,
+                        widget.collection,
+                        widget.user.email,
+                        role: CollectionParticipantRole.collaborator,
+                      );
+                      if ((result ?? false) && mounted) {
+                        widget.user.role = CollectionParticipantRole
+                            .collaborator
+                            .toStringVal();
+                        setState(() => {});
+                      }
+                    },
+              isBottomBorderRadiusRemoved: true,
+            ),
+            DividerWidget(
+              dividerType: DividerType.menu,
+              bgColor: getEnteColorScheme(context).fillFaint,
+            ),
+            MenuItemWidget(
+              captionedTextWidget: const CaptionedTextWidget(
+                title: "Viewer",
+              ),
+              leadingIcon: Icons.photo_outlined,
+              leadingIconColor: getEnteColorScheme(context).strokeBase,
+              menuItemColor: getEnteColorScheme(context).fillFaint,
+              pressedColor: getEnteColorScheme(context).fillFaint,
+              trailingIcon: widget.user.isViewer ? Icons.check : null,
+              onTap: widget.user.isViewer
+                  ? null
+                  : () async {
+                      final result =
+                          await collectionActions.addEmailToCollection(
+                        context,
+                        widget.collection,
+                        widget.user.email,
+                        role: CollectionParticipantRole.viewer,
+                      );
+                      if ((result ?? false) && mounted) {
+                        widget.user.role =
+                            CollectionParticipantRole.viewer.toStringVal();
+                        setState(() => {});
+                      }
+                    },
+              isTopBorderRadiusRemoved: true,
+            ),
+            const MenuSectionDescriptionWidget(
+              content:
+                  "Collaborators can add photos and videos to the shared album.",
+            ),
+            const SizedBox(height: 24),
+            const MenuSectionTitle(title: "Remove participant"),
+            MenuItemWidget(
+              captionedTextWidget: const CaptionedTextWidget(
+                title: "Remove",
+                textColor: warning500,
+                makeTextBold: true,
+              ),
+              leadingIcon: Icons.not_interested_outlined,
+              leadingIconColor: warning500,
+              menuItemColor: getEnteColorScheme(context).fillFaint,
+              pressedColor: getEnteColorScheme(context).fillFaint,
+              onTap: () async {
+                final result = await collectionActions.removeParticipant(
+                  context,
+                  widget.collection,
+                  widget.user,
+                );
+
+                if ((result ?? false) && mounted) {
+                  Navigator.of(context).pop(true);
+                }
+              },
+            )
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 32 - 6
lib/ui/sharing/manage_links_widget.dart

@@ -12,6 +12,7 @@ import 'package:photos/models/collection.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
@@ -47,6 +48,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
 
 
   Tuple3<int, String, int> _selectedExpiry;
   Tuple3<int, String, int> _selectedExpiry;
   int _selectedDeviceLimitIndex = 0;
   int _selectedDeviceLimitIndex = 0;
+  final CollectionActions sharingActions =
+      CollectionActions(CollectionsService.instance);
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -82,7 +85,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                     menuItemColor: getEnteColorScheme(context).fillFaint,
                     menuItemColor: getEnteColorScheme(context).fillFaint,
                     pressedColor: getEnteColorScheme(context).fillFaint,
                     pressedColor: getEnteColorScheme(context).fillFaint,
                     trailingWidget: Switch.adaptive(
                     trailingWidget: Switch.adaptive(
-                      value: widget.collection.publicURLs?.firstOrNull?.enableCollect ??
+                      value: widget.collection.publicURLs?.firstOrNull
+                              ?.enableCollect ??
                           false,
                           false,
                       onChanged: (value) async {
                       onChanged: (value) async {
                         await _updateUrlSettings(
                         await _updateUrlSettings(
@@ -96,7 +100,7 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                   ),
                   ),
                   const MenuSectionDescriptionWidget(
                   const MenuSectionDescriptionWidget(
                     content:
                     content:
-                    "Allow people with the link to also add photos to the shared "
+                        "Allow people with the link to also add photos to the shared "
                         "album.",
                         "album.",
                   ),
                   ),
                   const SizedBox(height: 24),
                   const SizedBox(height: 24),
@@ -139,8 +143,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                     },
                     },
                   ),
                   ),
                   DividerWidget(
                   DividerWidget(
-                    dividerType: DividerType.menu,
-                    bgColor: getEnteColorScheme(context).blurStrokeFaint,
+                    dividerType: DividerType.menuNoIcon,
+                    bgColor: getEnteColorScheme(context).fillFaint,
                   ),
                   ),
                   MenuItemWidget(
                   MenuItemWidget(
                     captionedTextWidget: const CaptionedTextWidget(
                     captionedTextWidget: const CaptionedTextWidget(
@@ -189,8 +193,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                     ),
                     ),
                   ),
                   ),
                   DividerWidget(
                   DividerWidget(
-                    dividerType: DividerType.menu,
-                    bgColor: getEnteColorScheme(context).blurStrokeFaint,
+                    dividerType: DividerType.menuNoIcon,
+                    bgColor: getEnteColorScheme(context).fillFaint,
                   ),
                   ),
                   MenuItemWidget(
                   MenuItemWidget(
                     captionedTextWidget: const CaptionedTextWidget(
                     captionedTextWidget: const CaptionedTextWidget(
@@ -229,6 +233,28 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                   const SizedBox(
                   const SizedBox(
                     height: 24,
                     height: 24,
                   ),
                   ),
+                  MenuItemWidget(
+                    captionedTextWidget: const CaptionedTextWidget(
+                      title: "Remove link",
+                      textColor: warning500,
+                      makeTextBold: true,
+                    ),
+                    leadingIcon: Icons.remove_circle_outline,
+                    leadingIconColor: warning500,
+                    menuItemColor: getEnteColorScheme(context).fillFaint,
+                    pressedColor: getEnteColorScheme(context).fillFaint,
+                    onTap: () async {
+                      final bool result = await sharingActions.publicLinkToggle(
+                        context,
+                        widget.collection,
+                        false,
+                      );
+                      if (result && mounted) {
+                        Navigator.of(context).pop();
+                        // setState(() => {});
+                      }
+                    },
+                  ),
                 ],
                 ],
               ),
               ),
             ),
             ),

+ 333 - 0
lib/ui/sharing/share_collection_page.dart

@@ -0,0 +1,333 @@
+// @dart=2.9
+
+import 'package:collection/collection.dart';
+import 'package:fast_base58/fast_base58.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_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/menu_section_title.dart';
+import 'package:photos/ui/sharing/add_partipant_page.dart';
+import 'package:photos/ui/sharing/album_participants_page.dart';
+import 'package:photos/ui/sharing/manage_links_widget.dart';
+import 'package:photos/ui/sharing/user_avator_widget.dart';
+import 'package:photos/utils/navigation_util.dart';
+import 'package:photos/utils/share_util.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class ShareCollectionPage extends StatefulWidget {
+  final Collection collection;
+
+  const ShareCollectionPage(this.collection, {Key key}) : super(key: key);
+
+  @override
+  State<ShareCollectionPage> createState() => _ShareCollectionPageState();
+}
+
+class _ShareCollectionPageState extends State<ShareCollectionPage> {
+  List<User> _sharees;
+  final Logger _logger = Logger("SharingDialogState");
+  final CollectionActions collectionActions =
+      CollectionActions(CollectionsService.instance);
+
+  Future<void> _navigateToManageUser() async {
+    final result = await routeToPage(
+      context,
+      AlbumParticipantsPage(widget.collection),
+    );
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    _sharees = widget.collection.sharees ?? [];
+    final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
+    final children = <Widget>[];
+    children.add(
+      MenuSectionTitle(
+        title: _sharees.isEmpty
+            ? "Share with specific people"
+            : "Shared with ${_sharees.length} ${_sharees.length == 1 ? 'person' : 'people'}",
+        iconData: Icons.workspaces,
+      ),
+    );
+
+    children.add(
+      EmailItemWidget(
+        widget.collection,
+        onTap: _navigateToManageUser,
+      ),
+    );
+
+    children.add(
+      MenuItemWidget(
+        captionedTextWidget: CaptionedTextWidget(
+          title: _sharees.isEmpty ? "Add email" : "Add more",
+          makeTextBold: true,
+        ),
+        leadingIcon: Icons.add,
+        menuItemColor: getEnteColorScheme(context).fillFaint,
+        pressedColor: getEnteColorScheme(context).fillFaint,
+        borderRadius: 4.0,
+        isTopBorderRadiusRemoved: _sharees.isNotEmpty,
+        onTap: () async {
+          routeToPage(context, AddParticipantPage(widget.collection)).then(
+            (value) => {
+              if (mounted) {setState(() => {})}
+            },
+          );
+        },
+      ),
+    );
+    if (_sharees.isEmpty && !hasUrl) {
+      children.add(
+        const MenuSectionDescriptionWidget(
+          content:
+              "Create shared and collaborative albums with other ente users, "
+              "including users on free plans.",
+        ),
+      );
+    }
+
+    final bool hasExpired =
+        widget.collection?.publicURLs?.firstOrNull?.isExpired ?? false;
+    children.addAll([
+      const SizedBox(
+        height: 24,
+      ),
+      MenuSectionTitle(
+        title: hasUrl
+            ? "Public link enabled"
+            : (_sharees.isEmpty ? "Or share a link" : "Share a link"),
+        iconData: Icons.public,
+      ),
+    ]);
+    if (hasUrl) {
+      if (hasExpired) {
+        children.add(
+          MenuItemWidget(
+            captionedTextWidget: CaptionedTextWidget(
+              title: "Link has expired",
+              textColor: getEnteColorScheme(context).warning500,
+            ),
+            leadingIcon: Icons.error_outline,
+            leadingIconColor: getEnteColorScheme(context).warning500,
+            menuItemColor: getEnteColorScheme(context).fillFaint,
+            pressedColor: getEnteColorScheme(context).fillFaint,
+            onTap: () async {},
+            isBottomBorderRadiusRemoved: true,
+          ),
+        );
+      } else {
+        final String collectionKey = Base58Encode(
+          CollectionsService.instance.getCollectionKey(widget.collection.id),
+        );
+        final String url =
+            "${widget.collection.publicURLs.first.url}#$collectionKey";
+        children.addAll(
+          [
+            MenuItemWidget(
+              captionedTextWidget: const CaptionedTextWidget(
+                title: "Copy link",
+                makeTextBold: true,
+              ),
+              leadingIcon: Icons.copy,
+              menuItemColor: getEnteColorScheme(context).fillFaint,
+              pressedColor: getEnteColorScheme(context).fillFaint,
+              onTap: () async {
+                await Clipboard.setData(ClipboardData(text: url));
+                showToast(context, "Link copied to clipboard");
+              },
+              isBottomBorderRadiusRemoved: true,
+            ),
+            DividerWidget(
+              dividerType: DividerType.menu,
+              bgColor: getEnteColorScheme(context).fillFaint,
+            ),
+            MenuItemWidget(
+              captionedTextWidget: const CaptionedTextWidget(
+                title: "Send link",
+                makeTextBold: true,
+              ),
+              leadingIcon: Icons.adaptive.share,
+              menuItemColor: getEnteColorScheme(context).fillFaint,
+              pressedColor: getEnteColorScheme(context).fillFaint,
+              onTap: () async {
+                shareText(url);
+              },
+              isTopBorderRadiusRemoved: true,
+              isBottomBorderRadiusRemoved: true,
+            ),
+          ],
+        );
+      }
+
+      children.addAll(
+        [
+          DividerWidget(
+            dividerType: DividerType.menu,
+            bgColor: getEnteColorScheme(context).fillFaint,
+          ),
+          MenuItemWidget(
+            captionedTextWidget: const CaptionedTextWidget(
+              title: "Manage link",
+              makeTextBold: true,
+            ),
+            leadingIcon: Icons.link,
+            trailingIcon: Icons.navigate_next,
+            menuItemColor: getEnteColorScheme(context).fillFaint,
+            pressedColor: getEnteColorScheme(context).fillFaint,
+            trailingIconIsMuted: true,
+            onTap: () async {
+              routeToPage(
+                context,
+                ManageSharedLinkWidget(collection: widget.collection),
+              ).then(
+                (value) => {
+                  if (mounted) {setState(() => {})}
+                },
+              );
+            },
+            isTopBorderRadiusRemoved: true,
+          ),
+        ],
+      );
+    } else {
+      children.add(
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Create public link",
+          ),
+          leadingIcon: Icons.link,
+          menuItemColor: getEnteColorScheme(context).fillFaint,
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          onTap: () async {
+            final bool result = await collectionActions.publicLinkToggle(
+              context,
+              widget.collection,
+              true,
+            );
+            if (result && mounted) {
+              setState(() => {});
+            }
+          },
+        ),
+      );
+      if (_sharees.isEmpty && !hasUrl) {
+        children.add(
+          const MenuSectionDescriptionWidget(
+            content:
+                "Links allow people without an ente account to view and add photos to your shared albums.",
+          ),
+        );
+      }
+    }
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          widget.collection.name,
+          style: Theme.of(context).textTheme.headline5.copyWith(fontSize: 16),
+        ),
+        elevation: 0,
+        centerTitle: false,
+      ),
+      body: SingleChildScrollView(
+        child: ListBody(
+          children: <Widget>[
+            Padding(
+              padding:
+                  const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16),
+              child: Column(
+                children: children,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class EmailItemWidget extends StatelessWidget {
+  final Collection collection;
+  final Function onTap;
+
+  const EmailItemWidget(
+    this.collection, {
+    this.onTap,
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    if (collection.getSharees().isEmpty) {
+      return const SizedBox.shrink();
+    } else if (collection.getSharees().length == 1) {
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: [
+          MenuItemWidget(
+            captionedTextWidget: CaptionedTextWidget(
+              title: collection.getSharees().firstOrNull?.email ?? '',
+            ),
+            leadingIconWidget: UserAvatarWidget(
+              collection.getSharees().first,
+              thumbnailView: true,
+            ),
+            leadingIconSize: 24,
+            menuItemColor: getEnteColorScheme(context).fillFaint,
+            pressedColor: getEnteColorScheme(context).fillFaint,
+            trailingIconIsMuted: true,
+            trailingIcon: Icons.chevron_right,
+            onTap: () async {
+              if (onTap != null) {
+                onTap();
+              }
+            },
+            isBottomBorderRadiusRemoved: true,
+          ),
+          DividerWidget(
+            dividerType: DividerType.menu,
+            bgColor: getEnteColorScheme(context).fillFaint,
+          ),
+        ],
+      );
+    } else {
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: [
+          MenuItemWidget(
+            captionedTextWidget: const CaptionedTextWidget(
+              title: 'Manage',
+            ),
+            leadingIcon: Icons.people_outline,
+            menuItemColor: getEnteColorScheme(context).fillFaint,
+            pressedColor: getEnteColorScheme(context).fillFaint,
+            trailingIconIsMuted: true,
+            trailingIcon: Icons.chevron_right,
+            onTap: () async {
+              if (onTap != null) {
+                onTap();
+              }
+            },
+            isBottomBorderRadiusRemoved: true,
+          ),
+          DividerWidget(
+            dividerType: DividerType.menu,
+            bgColor: getEnteColorScheme(context).fillFaint,
+          ),
+        ],
+      );
+    }
+  }
+}

+ 0 - 494
lib/ui/sharing/share_collection_widget.dart

@@ -1,494 +0,0 @@
-// @dart=2.9
-
-import 'dart:ui';
-
-import 'package:fast_base58/fast_base58.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_typeahead/flutter_typeahead.dart';
-import 'package:logging/logging.dart';
-import 'package:photos/core/configuration.dart';
-import 'package:photos/db/public_keys_db.dart';
-import 'package:photos/ente_theme_data.dart';
-import 'package:photos/models/collection.dart';
-import 'package:photos/models/public_key.dart';
-import 'package:photos/services/collections_service.dart';
-import 'package:photos/services/user_service.dart';
-import 'package:photos/ui/common/dialogs.dart';
-import 'package:photos/ui/common/gradient_button.dart';
-import 'package:photos/ui/common/loading_widget.dart';
-import 'package:photos/ui/payment/subscription.dart';
-import 'package:photos/ui/sharing/manage_links_widget.dart';
-import 'package:photos/utils/dialog_util.dart';
-import 'package:photos/utils/email_util.dart';
-import 'package:photos/utils/navigation_util.dart';
-import 'package:photos/utils/share_util.dart';
-import 'package:photos/utils/toast_util.dart';
-
-class SharingDialog extends StatefulWidget {
-  final Collection collection;
-
-  const SharingDialog(this.collection, {Key key}) : super(key: key);
-
-  @override
-  State<SharingDialog> createState() => _SharingDialogState();
-}
-
-class _SharingDialogState extends State<SharingDialog> {
-  bool _showEntryField = false;
-  List<User> _sharees;
-  String _email;
-  final Logger _logger = Logger("SharingDialogState");
-
-  @override
-  Widget build(BuildContext context) {
-    _sharees = widget.collection.sharees;
-    final children = <Widget>[];
-    if (!_showEntryField && _sharees.isEmpty) {
-      _showEntryField = true;
-    } else {
-      for (final user in _sharees) {
-        children.add(EmailItemWidget(widget.collection, user.email));
-      }
-    }
-    if (_showEntryField) {
-      children.add(_getEmailField());
-    }
-    children.add(
-      const Padding(
-        padding: EdgeInsets.all(8),
-      ),
-    );
-    if (!_showEntryField) {
-      children.add(
-        SizedBox(
-          width: 220,
-          child: GradientButton(
-            onTap: () async {
-              setState(() {
-                _showEntryField = true;
-              });
-            },
-            iconData: Icons.add,
-          ),
-        ),
-      );
-    } else {
-      children.add(
-        SizedBox(
-          width: 240,
-          height: 50,
-          child: OutlinedButton(
-            child: const Text("Add"),
-            onPressed: () {
-              _addEmailToCollection(_email?.trim() ?? '');
-            },
-          ),
-        ),
-      );
-    }
-
-    final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
-    children.addAll([
-      const Padding(padding: EdgeInsets.all(16)),
-      const Divider(height: 1),
-      const Padding(padding: EdgeInsets.all(12)),
-      SizedBox(
-        height: 36,
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            const Text("Public link"),
-            Switch(
-              value: hasUrl,
-              onChanged: (enable) async {
-                // confirm if user wants to disable the url
-                if (!enable) {
-                  final choice = await showChoiceDialog(
-                    context,
-                    'Disable link',
-                    'Are you sure that you want to disable the album link?',
-                    firstAction: 'Yes, disable',
-                    secondAction: 'No',
-                    actionType: ActionType.critical,
-                  );
-                  if (choice != DialogUserChoice.firstChoice) {
-                    return;
-                  }
-                }
-                final dialog = createProgressDialog(
-                  context,
-                  enable ? "Creating link..." : "Disabling link...",
-                );
-                try {
-                  await dialog.show();
-                  enable
-                      ? await CollectionsService.instance
-                          .createShareUrl(widget.collection)
-                      : await CollectionsService.instance
-                          .disableShareUrl(widget.collection);
-                  dialog.hide();
-                  setState(() {});
-                } catch (e) {
-                  dialog.hide();
-                  if (e is SharingNotPermittedForFreeAccountsError) {
-                    _showUnSupportedAlert();
-                  } else {
-                    _logger.severe("failed to share collection", e);
-                    showGenericErrorDialog(context);
-                  }
-                }
-              },
-            ),
-          ],
-        ),
-      ),
-      const Padding(padding: EdgeInsets.all(8)),
-    ]);
-    if (widget.collection.publicURLs?.isNotEmpty ?? false) {
-      children.add(
-        const Padding(
-          padding: EdgeInsets.all(2),
-        ),
-      );
-      children.add(_getShareableUrlWidget(context));
-    }
-
-    return AlertDialog(
-      title: const Text("Sharing"),
-      content: SingleChildScrollView(
-        child: ListBody(
-          children: <Widget>[
-            Padding(
-              padding: const EdgeInsets.all(4.0),
-              child: Column(
-                children: children,
-              ),
-            ),
-          ],
-        ),
-      ),
-      contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 4),
-    );
-  }
-
-  Widget _getEmailField() {
-    return Container(
-      padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
-      child: Row(
-        children: [
-          Expanded(
-            child: TypeAheadField(
-              textFieldConfiguration: const TextFieldConfiguration(
-                keyboardType: TextInputType.emailAddress,
-                decoration: InputDecoration(
-                  border: InputBorder.none,
-                  hintText: "email@your-friend.com",
-                ),
-              ),
-              hideOnEmpty: true,
-              loadingBuilder: (context) {
-                return const EnteLoadingWidget();
-              },
-              suggestionsCallback: (pattern) async {
-                _email = pattern;
-                return PublicKeysDB.instance.searchByEmail(_email);
-              },
-              itemBuilder: (context, suggestion) {
-                return Container(
-                  padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-                  child: Text(
-                    suggestion.email,
-                    overflow: TextOverflow.clip,
-                  ),
-                );
-              },
-              onSuggestionSelected: (PublicKey suggestion) {
-                _addEmailToCollection(
-                  suggestion.email,
-                  publicKey: suggestion.publicKey,
-                );
-              },
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  Widget _getShareableUrlWidget(BuildContext parentContext) {
-    final String collectionKey = Base58Encode(
-      CollectionsService.instance.getCollectionKey(widget.collection.id),
-    );
-    final String url =
-        "${widget.collection.publicURLs.first.url}#$collectionKey";
-    return SingleChildScrollView(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.start,
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          const Padding(padding: EdgeInsets.all(4)),
-          GestureDetector(
-            onTap: () async {
-              await Clipboard.setData(ClipboardData(text: url));
-              showToast(context, "Link copied to clipboard");
-            },
-            child: Container(
-              padding: const EdgeInsets.all(16),
-              color: Theme.of(context).colorScheme.onSurface.withOpacity(0.02),
-              child: Row(
-                crossAxisAlignment: CrossAxisAlignment.end,
-                children: [
-                  Flexible(
-                    child: Text(
-                      url,
-                      style: TextStyle(
-                        fontSize: 16,
-                        fontFeatures: const [FontFeature.tabularFigures()],
-                        color: Theme.of(context)
-                            .colorScheme
-                            .onSurface
-                            .withOpacity(0.68),
-                        overflow: TextOverflow.ellipsis,
-                      ),
-                    ),
-                  ),
-                  const Padding(padding: EdgeInsets.all(2)),
-                  const Icon(
-                    Icons.copy,
-                    size: 18,
-                  ),
-                ],
-              ),
-            ),
-          ),
-          const Padding(padding: EdgeInsets.all(2)),
-          TextButton(
-            child: Padding(
-              padding: const EdgeInsets.all(12),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: [
-                  Icon(
-                    Icons.adaptive.share,
-                    color: Theme.of(context).colorScheme.greenAlternative,
-                  ),
-                  const Padding(
-                    padding: EdgeInsets.all(4),
-                  ),
-                  Text(
-                    "Share link",
-                    style: TextStyle(
-                      color: Theme.of(context).colorScheme.greenAlternative,
-                    ),
-                  ),
-                ],
-              ),
-            ),
-            onPressed: () {
-              shareText(url);
-            },
-          ),
-          const Padding(padding: EdgeInsets.all(4)),
-          TextButton(
-            child: Center(
-              child: Text(
-                "Manage link",
-                style: TextStyle(
-                  color: Theme.of(context).primaryColorLight,
-                  decoration: TextDecoration.underline,
-                ),
-              ),
-            ),
-            onPressed: () async {
-              routeToPage(
-                parentContext,
-                ManageSharedLinkWidget(collection: widget.collection),
-              );
-            },
-          ),
-        ],
-      ),
-    );
-  }
-
-  Future<void> _addEmailToCollection(
-    String email, {
-    String publicKey,
-  }) async {
-    if (!isValidEmail(email)) {
-      showErrorDialog(
-        context,
-        "Invalid email address",
-        "Please enter a valid email address.",
-      );
-      return;
-    } else if (email == Configuration.instance.getEmail()) {
-      showErrorDialog(context, "Oops", "You cannot share with yourself");
-      return;
-    } else if (widget.collection.sharees.any((user) => user.email == email)) {
-      showErrorDialog(
-        context,
-        "Oops",
-        "You're already sharing this with " + email,
-      );
-      return;
-    }
-    if (publicKey == null) {
-      final dialog = createProgressDialog(context, "Searching for user...");
-      await dialog.show();
-
-      publicKey = await UserService.instance.getPublicKey(email);
-      await dialog.hide();
-    }
-    if (publicKey == null) {
-      Navigator.of(context, rootNavigator: true).pop('dialog');
-      final dialog = AlertDialog(
-        title: const Text("Invite to ente?"),
-        content: Text(
-          "Looks like " +
-              email +
-              " hasn't signed up for ente yet. would you like to invite them?",
-          style: const TextStyle(
-            height: 1.4,
-          ),
-        ),
-        actions: [
-          TextButton(
-            child: Text(
-              "Invite",
-              style: TextStyle(
-                color: Theme.of(context).colorScheme.greenAlternative,
-              ),
-            ),
-            onPressed: () {
-              shareText(
-                "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.",
-              );
-            },
-          ),
-        ],
-      );
-      showDialog(
-        context: context,
-        builder: (BuildContext context) {
-          return dialog;
-        },
-      );
-    } else {
-      final dialog = createProgressDialog(context, "Sharing...");
-      await dialog.show();
-      try {
-        await CollectionsService.instance
-            .share(widget.collection.id, email, publicKey);
-        await dialog.hide();
-        showShortToast(context, "Shared successfully!");
-        setState(() {
-          _sharees.add(User(email: email));
-          _showEntryField = false;
-        });
-      } catch (e) {
-        await dialog.hide();
-        if (e is SharingNotPermittedForFreeAccountsError) {
-          _showUnSupportedAlert();
-        } else {
-          _logger.severe("failed to share collection", e);
-          showGenericErrorDialog(context);
-        }
-      }
-    }
-  }
-
-  void _showUnSupportedAlert() {
-    final AlertDialog alert = AlertDialog(
-      title: const Text("Sorry"),
-      content: const Text(
-        "Sharing is not permitted for free accounts, please subscribe",
-      ),
-      actions: [
-        TextButton(
-          child: Text(
-            "Subscribe",
-            style: TextStyle(
-              color: Theme.of(context).colorScheme.greenAlternative,
-            ),
-          ),
-          onPressed: () {
-            Navigator.of(context).pushReplacement(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return getSubscriptionPage();
-                },
-              ),
-            );
-          },
-        ),
-        TextButton(
-          child: Text(
-            "Ok",
-            style: TextStyle(
-              color: Theme.of(context).colorScheme.onSurface,
-            ),
-          ),
-          onPressed: () {
-            Navigator.of(context, rootNavigator: true).pop();
-          },
-        ),
-      ],
-    );
-
-    showDialog(
-      context: context,
-      builder: (BuildContext context) {
-        return alert;
-      },
-    );
-  }
-}
-
-class EmailItemWidget extends StatelessWidget {
-  final Collection collection;
-  final String email;
-
-  const EmailItemWidget(
-    this.collection,
-    this.email, {
-    Key key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Row(
-      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-      children: [
-        Padding(
-          padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
-          child: Text(
-            email,
-            style: const TextStyle(fontSize: 16),
-          ),
-        ),
-        const Expanded(child: SizedBox()),
-        IconButton(
-          icon: const Icon(Icons.delete_forever),
-          color: Colors.redAccent,
-          onPressed: () async {
-            final dialog = createProgressDialog(context, "Please wait...");
-            await dialog.show();
-            try {
-              await CollectionsService.instance.unshare(collection.id, email);
-              collection.sharees.removeWhere((user) => user.email == email);
-              await dialog.hide();
-              showToast(context, "Stopped sharing with " + email + ".");
-              Navigator.of(context).pop();
-            } catch (e, s) {
-              Logger("EmailItemWidget").severe(e, s);
-              await dialog.hide();
-              showGenericErrorDialog(context);
-            }
-          },
-        ),
-      ],
-    );
-  }
-}

+ 81 - 0
lib/ui/sharing/user_avator_widget.dart

@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:tuple/tuple.dart';
+
+enum AvatarType { small, mini, tiny, extra }
+
+class UserAvatarWidget extends StatelessWidget {
+  final User user;
+  final AvatarType type;
+  final int currentUserID;
+  final bool thumbnailView;
+
+  const UserAvatarWidget(
+    this.user, {
+    super.key,
+    this.currentUserID = -1,
+    this.type = AvatarType.mini,
+    this.thumbnailView = false,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final enteTextTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+    final displayChar = (user.name == null || user.name!.isEmpty)
+        ? ((user.email.isEmpty) ? " " : user.email.substring(0, 1))
+        : user.name!.substring(0, 1);
+    final randomColor = colorScheme.avatarColors[
+        (user.id ?? 0).remainder(colorScheme.avatarColors.length)];
+    final Color decorationColor =
+        ((user.id ?? -1) == currentUserID) ? Colors.black : randomColor;
+
+    final avatarStyle = getAvatarStyle(context, type);
+    final double size = avatarStyle.item1;
+    final TextStyle textStyle = avatarStyle.item2;
+    return Container(
+      height: size,
+      width: size,
+      padding: thumbnailView
+          ? const EdgeInsets.only(bottom: 1)
+          : const EdgeInsets.all(2),
+      decoration: thumbnailView
+          ? null
+          : BoxDecoration(
+              shape: BoxShape.circle,
+              color: decorationColor,
+              border: Border.all(
+                color: strokeBaseDark,
+                width: 1.0,
+              ),
+            ),
+      child: CircleAvatar(
+        backgroundColor: decorationColor,
+        child: Text(
+          displayChar.toUpperCase(),
+          // fixed color
+          style: textStyle.copyWith(color: Colors.white),
+        ),
+      ),
+    );
+  }
+
+  Tuple2<double, TextStyle> getAvatarStyle(
+    BuildContext context,
+    AvatarType type,
+  ) {
+    final enteTextTheme = getEnteTextTheme(context);
+    switch (type) {
+      case AvatarType.small:
+        return Tuple2(36.0, enteTextTheme.small);
+      case AvatarType.mini:
+        return Tuple2(24.0, enteTextTheme.mini);
+      case AvatarType.tiny:
+        return Tuple2(18.0, enteTextTheme.tiny);
+      case AvatarType.extra:
+        return Tuple2(18.0, enteTextTheme.tiny);
+    }
+  }
+}

+ 17 - 13
lib/ui/tools/debug/cache_size_view.dart

@@ -15,7 +15,7 @@ class CacheSizeViewer extends StatefulWidget {
 }
 }
 
 
 class _CacheSizeViewerState extends State<CacheSizeViewer> {
 class _CacheSizeViewerState extends State<CacheSizeViewer> {
-  final List<String> paths = [];
+  final List<PathStorageItem> paths = [];
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -37,18 +37,24 @@ class _CacheSizeViewerState extends State<CacheSizeViewer> {
     final videoCachePath =
     final videoCachePath =
         appTemporaryDirectory.path + "/" + VideoCacheManager.key;
         appTemporaryDirectory.path + "/" + VideoCacheManager.key;
     paths.addAll([
     paths.addAll([
-      tempDownload,
-      cacheDirectory,
-      logsDirectory,
-      imageCachePath,
-      videoCachePath,
-      iOSOnlyTempDirectory + "flutter-images",
-      appDocumentsDirectory.path,
-      appSupportDirectory.path,
-      appTemporaryDirectory.path,
+      PathStorageItem.name(imageCachePath, "Remote images"),
+      PathStorageItem.name(videoCachePath, "Remote videos"),
+      PathStorageItem.name(cacheDirectory, "Remote thumbnails"),
+      PathStorageItem.name(tempDownload, "Pending sync"),
+      PathStorageItem.name(logsDirectory, "Application logs"),
+      PathStorageItem.name(
+          iOSOnlyTempDirectory + "flutter-images", "Local Gallery"),
+      PathStorageItem.name(appDocumentsDirectory.path, "App Documents"),
+      PathStorageItem.name(appSupportDirectory.path, "App Support"),
+      PathStorageItem.name(appTemporaryDirectory.path, "App Temporary"),
     ]);
     ]);
     appTemporaryDirectory.list().forEach((element) {
     appTemporaryDirectory.list().forEach((element) {
-      paths.add(element.path);
+      paths.add(
+        PathStorageItem.name(
+          element.path,
+          "App Temp " + element.path.substring(element.path.length - 10),
+        ),
+      );
     });
     });
     if (mounted) {
     if (mounted) {
       setState(() => {});
       setState(() => {});
@@ -72,13 +78,11 @@ class _CacheSizeViewerState extends State<CacheSizeViewer> {
       child: SingleChildScrollView(
       child: SingleChildScrollView(
         child: ListView.builder(
         child: ListView.builder(
           shrinkWrap: true,
           shrinkWrap: true,
-
           padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
           padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
           physics: const ScrollPhysics(),
           physics: const ScrollPhysics(),
           // to disable GridView's scrolling
           // to disable GridView's scrolling
           itemBuilder: (context, index) {
           itemBuilder: (context, index) {
             final path = paths[index];
             final path = paths[index];
-            // return Text(path);
             return PathStorageViewer(path);
             return PathStorageViewer(path);
           },
           },
           itemCount: paths.length,
           itemCount: paths.length,

+ 36 - 30
lib/ui/tools/debug/path_storage_viewer.dart

@@ -1,22 +1,33 @@
 // @dart=2.9
 // @dart=2.9
 
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.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';
 import 'package:photos/ui/components/menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
-import 'package:photos/ui/components/menu_section_description_widget.dart';
 import 'package:photos/utils/app_size.dart';
 import 'package:photos/utils/app_size.dart';
 import 'package:photos/utils/data_util.dart';
 import 'package:photos/utils/data_util.dart';
 
 
-class PathStorageViewer extends StatefulWidget {
+class PathStorageItem {
   final String path;
   final String path;
-  final bool allowClear;
+  final String title;
+  final bool allowCacheClear;
+
+  PathStorageItem.name(
+    this.path,
+    this.title, {
+    this.allowCacheClear = false,
+  });
+}
+
+class PathStorageViewer extends StatefulWidget {
+  final PathStorageItem path;
 
 
   const PathStorageViewer(
   const PathStorageViewer(
     this.path, {
     this.path, {
     Key key,
     Key key,
-    this.allowClear = false,
   }) : super(key: key);
   }) : super(key: key);
 
 
   @override
   @override
@@ -28,7 +39,7 @@ class _PathStorageViewerState extends State<PathStorageViewer> {
 
 
   @override
   @override
   void initState() {
   void initState() {
-    directoryStat(widget.path).then((logs) {
+    directoryStat(widget.path.path).then((logs) {
       setState(() {
       setState(() {
         _logs = logs;
         _logs = logs;
       });
       });
@@ -47,33 +58,28 @@ class _PathStorageViewerState extends State<PathStorageViewer> {
     }
     }
     final int fileCount = _logs["fileCount"] ?? -1;
     final int fileCount = _logs["fileCount"] ?? -1;
     final int size = _logs['size'];
     final int size = _logs['size'];
-    var pathVale = widget.path;
-    if (pathVale.endsWith("/")) {
-      pathVale = pathVale.substring(0, pathVale.length - 1);
-    }
-    int pathEle = pathVale.split("/").length;
-    final title =
-        pathVale.split("/")[pathEle - 2] + "/" + pathVale.split("/").last;
 
 
-    return Padding(
-      padding: const EdgeInsets.all(8.0),
-      child: Column(
-        children: [
-          MenuItemWidget(
-            alignCaptionedTextToLeft: true,
-            captionedTextWidget: CaptionedTextWidget(
-              title: title,
-              subTitle: '$fileCount - ${formatBytes(size)}',
-            ),
-            trailingIcon: widget.allowClear ? Icons.chevron_right : null,
-            menuItemColor: getEnteColorScheme(context).fillFaint,
-            onTap: () async {
-              // await showPicker();
-            },
-          ),
-          MenuSectionDescriptionWidget(content: widget.path)
-        ],
+    return MenuItemWidget(
+      alignCaptionedTextToLeft: true,
+      captionedTextWidget: CaptionedTextWidget(
+        title: widget.path.title,
+        subTitle: '$fileCount',
+        subTitleColor: getEnteColorScheme(context).textFaint,
+      ),
+      trailingWidget: Text(
+        formatBytes(size),
+        style: getEnteTextTheme(context)
+            .small
+            .copyWith(color: getEnteColorScheme(context).textFaint),
       ),
       ),
+      trailingIcon: widget.path.allowCacheClear ? Icons.chevron_right : null,
+      menuItemColor: getEnteColorScheme(context).fillFaint,
+      onTap: () async {
+        if (kDebugMode) {
+          await Clipboard.setData(ClipboardData(text: widget.path.path));
+          debugPrint(widget.path.path);
+        }
+      },
     );
     );
   }
   }
 }
 }

+ 317 - 0
lib/ui/viewer/actions/file_selection_actions_widget.dart

@@ -0,0 +1,317 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:page_transition/page_transition.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/models/device_collection.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/models/selected_file_breakup.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/hidden_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/actions/collection/collection_file_actions.dart';
+import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
+import 'package:photos/ui/components/blur_menu_item_widget.dart';
+import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
+import 'package:photos/ui/create_collection_page.dart';
+import 'package:photos/utils/delete_file_util.dart';
+import 'package:photos/utils/magic_util.dart';
+
+class FileSelectionActionWidget extends StatefulWidget {
+  final GalleryType type;
+  final Collection? collection;
+  final DeviceCollection? deviceCollection;
+  final SelectedFiles selectedFiles;
+
+  const FileSelectionActionWidget(
+    this.type,
+    this.selectedFiles, {
+    Key? key,
+    this.collection,
+    this.deviceCollection,
+  }) : super(key: key);
+
+  @override
+  State<FileSelectionActionWidget> createState() =>
+      _FileSelectionActionWidgetState();
+}
+
+class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
+  late int currentUserID;
+  late SelectedFileSplit split;
+  late CollectionActions collectionActions;
+
+  @override
+  void initState() {
+    currentUserID = Configuration.instance.getUserID()!;
+    split = widget.selectedFiles.split(currentUserID);
+    widget.selectedFiles.addListener(_selectFileChangeListener);
+    collectionActions = CollectionActions(CollectionsService.instance);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.selectedFiles.removeListener(_selectFileChangeListener);
+    super.dispose();
+  }
+
+  void _selectFileChangeListener() {
+    split = widget.selectedFiles.split(currentUserID);
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final bool showPrefix =
+        split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty;
+    final String suffix = showPrefix
+        ? " (${split.ownedByCurrentUser.length})"
+            ""
+        : "";
+    final String suffixInPending = split.ownedByOtherUsers.isNotEmpty
+        ? " (${split.ownedByCurrentUser.length + split.pendingUploads.length})"
+            ""
+        : "";
+    final bool anyOwnedFiles =
+        split.pendingUploads.isNotEmpty || split.ownedByCurrentUser.isNotEmpty;
+    final bool anyUploadedFiles = split.ownedByCurrentUser.isNotEmpty;
+    bool showRemoveOption = widget.type.showRemoveFromAlbum();
+    if (showRemoveOption && widget.type == GalleryType.sharedCollection) {
+      showRemoveOption = split.ownedByCurrentUser.isNotEmpty;
+    }
+    debugPrint('$runtimeType building  $mounted');
+    final colorScheme = getEnteColorScheme(context);
+    final List<List<BlurMenuItemWidget>> items = [];
+    final List<BlurMenuItemWidget> firstList = [];
+    final showUploadIcon = widget.type == GalleryType.localFolder &&
+        split.ownedByCurrentUser.isEmpty;
+    if (widget.type.showAddToAlbum()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon:
+              showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
+          labelText:
+              "Add to ${showUploadIcon ? 'ente' : 'album'}$suffixInPending",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyOwnedFiles ? _addToAlbum : null,
+        ),
+      );
+    }
+    if (widget.type.showMoveToAlbum()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.arrow_forward_outlined,
+          labelText: "Move to album$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyUploadedFiles ? _moveFiles : null,
+        ),
+      );
+    }
+
+    if (showRemoveOption) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.remove_outlined,
+          labelText: "Remove from album$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyUploadedFiles ? _removeFilesFromAlbum : null,
+        ),
+      );
+    }
+
+    if (widget.type.showDeleteOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.delete_outline,
+          labelText: "Delete$suffixInPending",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyOwnedFiles ? _onDeleteClick : null,
+        ),
+      );
+    }
+
+    if (widget.type.showHideOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.visibility_off_outlined,
+          labelText: "Hide$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyUploadedFiles ? _onHideClick : null,
+        ),
+      );
+    } else if (widget.type.showUnHideOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.visibility_off_outlined,
+          labelText: "Unhide$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: _onUnhideClick,
+        ),
+      );
+    }
+    if (widget.type.showArchiveOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.archive_outlined,
+          labelText: "Archive$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyUploadedFiles ? _onArchiveClick : null,
+        ),
+      );
+    } else if (widget.type.showUnArchiveOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.unarchive,
+          labelText: "Unarchive$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: _onUnArchiveClick,
+        ),
+      );
+    }
+
+    if (widget.type.showFavoriteOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.favorite_border_rounded,
+          labelText: "Favorite$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: anyUploadedFiles ? _onFavoriteClick : null,
+        ),
+      );
+    } else if (widget.type.showUnFavoriteOption()) {
+      firstList.add(
+        BlurMenuItemWidget(
+          leadingIcon: Icons.favorite,
+          labelText: "Remove from favorite$suffix",
+          menuItemColor: colorScheme.fillFaint,
+          onTap: _onUnFavoriteClick,
+        ),
+      );
+    }
+
+    if (firstList.isNotEmpty) {
+      items.add(firstList);
+      return ExpandedMenuWidget(
+        items: items,
+      );
+    } else {
+      // TODO: Return "Select All" here
+      return const SizedBox.shrink();
+    }
+  }
+
+  Future<void> _moveFiles() async {
+    if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
+      widget.selectedFiles
+          .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
+      widget.selectedFiles
+          .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
+    }
+    await _selectionCollectionForAction(CollectionActionType.moveFiles);
+  }
+
+  Future<void> _addToAlbum() async {
+    if (split.ownedByOtherUsers.isNotEmpty) {
+      widget.selectedFiles
+          .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
+    }
+    await _selectionCollectionForAction(CollectionActionType.addFiles);
+  }
+
+  Future<void> _onDeleteClick() async {
+    showDeleteSheet(context, widget.selectedFiles);
+  }
+
+  Future<void> _removeFilesFromAlbum() async {
+    if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
+      widget.selectedFiles
+          .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
+      widget.selectedFiles
+          .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
+    }
+    await collectionActions.showRemoveFromCollectionSheet(
+      context,
+      widget.collection!,
+      widget.selectedFiles,
+    );
+  }
+
+  Future<void> _onFavoriteClick() async {
+    final result = await collectionActions.updateFavorites(
+      context,
+      split.ownedByCurrentUser,
+      true,
+    );
+    if (result) {
+      widget.selectedFiles.clearAll();
+    }
+  }
+
+  Future<void> _onUnFavoriteClick() async {
+    final result = await collectionActions.updateFavorites(
+      context,
+      split.ownedByCurrentUser,
+      false,
+    );
+    if (result) {
+      widget.selectedFiles.clearAll();
+    }
+  }
+
+  Future<void> _onArchiveClick() async {
+    await changeVisibility(
+      context,
+      split.ownedByCurrentUser,
+      visibilityArchive,
+    );
+    widget.selectedFiles.clearAll();
+  }
+
+  Future<void> _onUnArchiveClick() async {
+    await changeVisibility(
+      context,
+      split.ownedByCurrentUser,
+      visibilityVisible,
+    );
+    widget.selectedFiles.clearAll();
+  }
+
+  Future<void> _onHideClick() async {
+    await CollectionsService.instance.hideFiles(
+      context,
+      split.ownedByCurrentUser,
+    );
+    widget.selectedFiles.clearAll();
+  }
+
+  Future<void> _onUnhideClick() async {
+    if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
+      widget.selectedFiles
+          .unSelectAll(split.pendingUploads.toSet(), skipNotify: true);
+      widget.selectedFiles
+          .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
+    }
+    await _selectionCollectionForAction(CollectionActionType.unHide);
+  }
+
+  Future<Object?> _selectionCollectionForAction(
+    CollectionActionType type,
+  ) async {
+    return Navigator.push(
+      context,
+      PageTransition(
+        type: PageTransitionType.bottomToTop,
+        child: CreateCollectionPage(
+          widget.selectedFiles,
+          null,
+          actionType: type,
+        ),
+      ),
+    );
+  }
+}

+ 16 - 0
lib/ui/viewer/actions/file_selection_common_actions_widget.dart

@@ -0,0 +1,16 @@
+import 'package:flutter/cupertino.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/selected_files.dart';
+
+class FileSelectionCommonActionWidget extends StatelessWidget {
+  final GalleryType type;
+  final SelectedFiles selectedFiles;
+
+  const FileSelectionCommonActionWidget(
+      {super.key, required this.type, required this.selectedFiles});
+  @override
+  Widget build(BuildContext context) {
+    // TODO: implement build
+    throw UnimplementedError();
+  }
+}

+ 167 - 0
lib/ui/viewer/actions/file_selection_overlay_bar.dart

@@ -0,0 +1,167 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:page_transition/page_transition.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/models/device_collection.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/create_collection_page.dart';
+import 'package:photos/ui/viewer/actions/file_selection_actions_widget.dart';
+import 'package:photos/utils/delete_file_util.dart';
+import 'package:photos/utils/magic_util.dart';
+import 'package:photos/utils/share_util.dart';
+
+class FileSelectionOverlayBar extends StatefulWidget {
+  final GalleryType galleryType;
+  final SelectedFiles selectedFiles;
+  final Collection? collection;
+  final DeviceCollection? deviceCollection;
+
+  const FileSelectionOverlayBar(
+    this.galleryType,
+    this.selectedFiles, {
+    this.collection,
+    this.deviceCollection,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<FileSelectionOverlayBar> createState() =>
+      _FileSelectionOverlayBarState();
+}
+
+class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
+  final GlobalKey shareButtonKey = GlobalKey();
+  final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
+  late bool showDeleteOption;
+
+  @override
+  void initState() {
+    showDeleteOption = widget.galleryType.showDeleteIconOption();
+    widget.selectedFiles.addListener(_selectedFilesListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.selectedFiles.removeListener(_selectedFilesListener);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    debugPrint(
+        '$runtimeType building with ${widget.selectedFiles.files.length}');
+    final List<IconButtonWidget> iconsButton = [];
+    final iconColor = getEnteColorScheme(context).blurStrokeBase;
+    if (showDeleteOption) {
+      iconsButton.add(
+        IconButtonWidget(
+          icon: Icons.delete_outlined,
+          iconButtonType: IconButtonType.primary,
+          iconColor: iconColor,
+          onTap: () => showDeleteSheet(context, widget.selectedFiles),
+        ),
+      );
+    }
+
+    if (widget.galleryType.showUnArchiveOption()) {
+      iconsButton.add(
+        IconButtonWidget(
+          icon: Icons.unarchive,
+          iconButtonType: IconButtonType.primary,
+          iconColor: iconColor,
+          onTap: () => _onUnArchiveClick(),
+        ),
+      );
+    }
+    if (widget.galleryType.showUnHideOption()) {
+      iconsButton.add(
+        IconButtonWidget(
+          icon: Icons.visibility_off_outlined,
+          iconButtonType: IconButtonType.primary,
+          iconColor: iconColor,
+          onTap: () => _selectionCollectionForAction(
+            CollectionActionType.unHide,
+          ),
+        ),
+      );
+    }
+    iconsButton.add(
+      IconButtonWidget(
+        icon: Icons.adaptive.share_outlined,
+        iconButtonType: IconButtonType.primary,
+        iconColor: getEnteColorScheme(context).blurStrokeBase,
+        onTap: () => shareSelected(
+          context,
+          shareButtonKey,
+          widget.selectedFiles.files,
+        ),
+      ),
+    );
+    return ValueListenableBuilder(
+      valueListenable: _bottomPosition,
+      builder: (context, value, child) {
+        return AnimatedPositioned(
+          curve: Curves.easeInOutExpo,
+          bottom: _bottomPosition.value,
+          right: 0,
+          left: 0,
+          duration: const Duration(milliseconds: 400),
+          child: BottomActionBarWidget(
+            selectedFiles: widget.selectedFiles,
+            hasSmallerBottomPadding: true,
+            type: widget.galleryType,
+            expandedMenu: FileSelectionActionWidget(
+              widget.galleryType,
+              widget.selectedFiles,
+              collection: widget.collection,
+            ),
+            text: widget.selectedFiles.files.length.toString() + ' selected',
+            onCancel: () {
+              if (widget.selectedFiles.files.isNotEmpty) {
+                widget.selectedFiles.clearAll();
+              }
+            },
+            iconButtons: iconsButton,
+          ),
+        );
+      },
+    );
+  }
+
+  Future<void> _onUnArchiveClick() async {
+    await changeVisibility(
+      context,
+      widget.selectedFiles.files.toList(),
+      visibilityVisible,
+    );
+    widget.selectedFiles.clearAll();
+  }
+
+  Future<Object?> _selectionCollectionForAction(
+    CollectionActionType type,
+  ) async {
+    return Navigator.push(
+      context,
+      PageTransition(
+        type: PageTransitionType.bottomToTop,
+        child: CreateCollectionPage(
+          widget.selectedFiles,
+          null,
+          actionType: type,
+        ),
+      ),
+    );
+  }
+
+  _selectedFilesListener() {
+    widget.selectedFiles.files.isNotEmpty
+        ? _bottomPosition.value = 0.0
+        : _bottomPosition.value = -150.0;
+  }
+}

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

@@ -1,9 +1,8 @@
-// @dart=2.9
-
 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.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/collection_items.dart';
+import 'package:photos/models/gallery_type.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/viewer/file/file_info_collection_widget.dart';
 import 'package:photos/ui/viewer/file/file_info_collection_widget.dart';
@@ -12,17 +11,20 @@ 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;
+  final int currentUserID;
 
 
-  const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
+  const CollectionsListOfFileWidget(
+      this.allCollectionIDsOfFile, this.currentUserID,
+      {Key? key})
       : super(key: key);
       : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return FutureBuilder(
+    return FutureBuilder<Set<int>>(
       future: allCollectionIDsOfFile,
       future: allCollectionIDsOfFile,
       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 = <Collection>[];
           final collections = <Collection>[];
           for (var collectionID in collectionIDs) {
           for (var collectionID in collectionIDs) {
             final c =
             final c =
@@ -44,6 +46,9 @@ class CollectionsListOfFileWidget extends StatelessWidget {
                     context,
                     context,
                     CollectionPage(
                     CollectionPage(
                       CollectionWithThumbnail(collections[index], null),
                       CollectionWithThumbnail(collections[index], null),
+                      appBarType: collections[index].isOwner(currentUserID)
+                          ? GalleryType.ownedCollection
+                          : GalleryType.sharedCollection,
                     ),
                     ),
                   );
                   );
                 },
                 },

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

@@ -286,7 +286,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
       topControl: const SizedBox.shrink(),
       topControl: const SizedBox.shrink(),
       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
       backgroundColor: colorScheme.backgroundElevated,
       backgroundColor: colorScheme.backgroundElevated,
-      barrierColor: backdropMutedDark,
+      barrierColor: backdropFaintDark,
       context: context,
       context: context,
       builder: (BuildContext context) {
       builder: (BuildContext context) {
         return Padding(
         return Padding(

+ 41 - 2
lib/ui/viewer/file/file_caption_widget.dart

@@ -6,8 +6,48 @@ import 'package:photos/ui/components/keyboard/keybiard_oveylay.dart';
 import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
 import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/magic_util.dart';
 
 
+class FileCaptionReadyOnly extends StatelessWidget {
+  final String caption;
+
+  const FileCaptionReadyOnly({super.key, required this.caption});
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    final textTheme = getEnteTextTheme(context);
+    return Padding(
+      padding: const EdgeInsets.all(4.0),
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(
+          minHeight: 32.0,
+          minWidth: double.infinity,
+          maxHeight: 200.0,
+          maxWidth: double.infinity,
+        ),
+        child: DecoratedBox(
+          decoration: BoxDecoration(
+            color: colorScheme.fillFaint,
+            borderRadius: BorderRadius.circular(4),
+          ),
+          child: SingleChildScrollView(
+            scrollDirection: Axis.vertical,
+            child: Padding(
+              padding: const EdgeInsets.all(12.0),
+              child: Text(
+                caption,
+                style: textTheme.small,
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
 class FileCaptionWidget extends StatefulWidget {
 class FileCaptionWidget extends StatefulWidget {
   final File file;
   final File file;
+
   const FileCaptionWidget({required this.file, super.key});
   const FileCaptionWidget({required this.file, super.key});
 
 
   @override
   @override
@@ -16,6 +56,7 @@ class FileCaptionWidget extends StatefulWidget {
 
 
 class _FileCaptionWidgetState extends State<FileCaptionWidget> {
 class _FileCaptionWidgetState extends State<FileCaptionWidget> {
   static const int maxLength = 5000;
   static const int maxLength = 5000;
+
   // counterThreshold represents the nun of char after which
   // counterThreshold represents the nun of char after which
   // currentLength/maxLength will show up
   // currentLength/maxLength will show up
   static const int counterThreshold = 1000;
   static const int counterThreshold = 1000;
@@ -138,8 +179,6 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
     if (hasFocus) {
     if (hasFocus) {
       KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
       KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
     } else {
     } else {
-      debugPrint("Removing listener");
-
       KeyboardOverlay.removeOverlay();
       KeyboardOverlay.removeOverlay();
     }
     }
   }
   }

+ 180 - 64
lib/ui/viewer/file/file_icons_widget.dart

@@ -1,7 +1,9 @@
 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/models/collection.dart';
 import 'package:photos/models/trash_file.dart';
 import 'package:photos/models/trash_file.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/colors.dart';
+import 'package:photos/ui/sharing/user_avator_widget.dart';
 import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/date_time_util.dart';
 
 
 class ThumbnailPlaceHolder extends StatelessWidget {
 class ThumbnailPlaceHolder extends StatelessWidget {
@@ -21,47 +23,30 @@ class UnSyncedIcon extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return Container(
-      decoration: const BoxDecoration(
-        gradient: LinearGradient(
-          begin: Alignment.centerLeft,
-          end: Alignment.centerRight,
-          // background: linear-gradient(73.58deg, rgba(0, 0, 0, 0.3) -6.66%, rgba(255, 255, 255, 0) 44.44%);
-          colors: [
-            Color.fromRGBO(255, 255, 255, 0),
-            Colors.transparent,
-            // Color.fromRGBO(0, 0, 0, 0.3),
-          ],
-          stops: [-0.067, 0.445],
-        ),
-      ),
-      child: const Align(
-        alignment: Alignment.bottomLeft,
-        child: Padding(
-          padding: EdgeInsets.only(left: 4, bottom: 4),
-          child: Icon(
-            Icons.cloud_off_outlined,
-            size: 18,
-            color: fixedStrokeMutedWhite,
-          ),
-        ),
-      ),
+    return const _BottomLeftOverlayIcon(Icons.cloud_off_outlined);
+  }
+}
+
+class FavoriteOverlayIcon extends StatelessWidget {
+  const FavoriteOverlayIcon({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return const _BottomLeftOverlayIcon(
+      Icons.favorite_rounded,
+      baseSize: 22,
     );
     );
   }
   }
 }
 }
 
 
-class VideoOverlayIcon extends StatelessWidget {
-  const VideoOverlayIcon({Key? key}) : super(key: key);
+class ArchiveOverlayIcon extends StatelessWidget {
+  const ArchiveOverlayIcon({Key? key}) : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return const SizedBox(
-      height: 64,
-      child: Icon(
-        Icons.play_circle_outline,
-        size: 40,
-        color: Colors.white70,
-      ),
+    return const _BottomLeftOverlayIcon(
+      Icons.archive_outlined,
+      color: fixedStrokeMutedWhite,
     );
     );
   }
   }
 }
 }
@@ -71,33 +56,43 @@ class LivePhotoOverlayIcon extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return const Align(
-      alignment: Alignment.bottomRight,
-      child: Padding(
-        padding: EdgeInsets.only(right: 4, bottom: 4),
-        child: Icon(
-          Icons.album_outlined,
-          size: 14,
-          color: Colors.white, // fixed
-        ),
+    return const _BottomRightOverlayIcon(
+      Icons.album_outlined,
+      baseSize: 18,
+    );
+  }
+}
+
+class VideoOverlayIcon extends StatelessWidget {
+  const VideoOverlayIcon({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return const SizedBox(
+      height: 64,
+      child: Icon(
+        Icons.play_circle_outline,
+        size: 40,
+        color: Colors.white70,
       ),
       ),
     );
     );
   }
   }
 }
 }
 
 
-class FavoriteOverlayIcon extends StatelessWidget {
-  const FavoriteOverlayIcon({Key? key}) : super(key: key);
+class OwnerAvatarOverlayIcon extends StatelessWidget {
+  final User user;
+  const OwnerAvatarOverlayIcon(this.user, {Key? key}) : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return const Align(
-      alignment: Alignment.bottomLeft,
+    return Align(
+      alignment: Alignment.topRight,
       child: Padding(
       child: Padding(
-        padding: EdgeInsets.only(left: 4, bottom: 4),
-        child: Icon(
-          Icons.favorite_rounded,
-          size: 20,
-          color: Colors.white, // fixed
+        padding: const EdgeInsets.only(right: 4, top: 4),
+        child: UserAvatarWidget(
+          user,
+          type: AvatarType.tiny,
+          thumbnailView: true,
         ),
         ),
       ),
       ),
     );
     );
@@ -132,21 +127,142 @@ class TrashedFileOverlayText extends StatelessWidget {
   }
   }
 }
 }
 
 
-class ArchiveOverlayIcon extends StatelessWidget {
-  const ArchiveOverlayIcon({Key? key}) : super(key: key);
+// Base variations
+
+/// Icon overlay in the bottom left.
+///
+/// This usually indicates ente specific state of a file, e.g. if it is
+/// favorited/archived.
+class _BottomLeftOverlayIcon extends StatelessWidget {
+  final IconData icon;
+
+  /// Overriddable color. Default is a fixed white.
+  final Color color;
+
+  /// Overriddable default size. This is just the initial hint, the actual size
+  /// is dynamic based on the widget's width (so that we show smaller icons in
+  /// smaller thumbnails).
+  final double baseSize;
+
+  const _BottomLeftOverlayIcon(
+    this.icon, {
+    Key? key,
+    this.baseSize = 24,
+    this.color = Colors.white, // fixed
+  }) : super(key: key);
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return const Align(
-      alignment: Alignment.bottomLeft,
-      child: Padding(
-        padding: EdgeInsets.only(left: 4, bottom: 4),
-        child: Icon(
-          Icons.archive_outlined,
-          size: 20,
-          color: fixedStrokeMutedWhite,
-        ),
-      ),
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        double inset = 4;
+        double size = baseSize;
+
+        if (constraints.hasBoundedWidth) {
+          final w = constraints.maxWidth;
+          if (w > 120) {
+            size = 24;
+          } else if (w < 75) {
+            inset = 3;
+            size = 16;
+          }
+        }
+
+        return Container(
+          decoration: const BoxDecoration(
+            gradient: LinearGradient(
+              begin: Alignment.bottomLeft,
+              end: Alignment.center,
+              colors: [
+                Color.fromRGBO(0, 0, 0, 0.14),
+                Color.fromRGBO(0, 0, 0, 0.05),
+                Color.fromRGBO(0, 0, 0, 0.0),
+              ],
+              stops: [0, 0.6, 1],
+            ),
+          ),
+          child: Align(
+            alignment: Alignment.bottomLeft,
+            child: Padding(
+              padding: EdgeInsets.only(left: inset, bottom: inset),
+              child: Icon(
+                icon,
+                size: size,
+                color: color,
+              ),
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
+
+/// Icon overlay in the bottom right.
+///
+/// This usually indicates information about the file itself, e.g. whether it is
+/// a live photo, or the duration of the video.
+class _BottomRightOverlayIcon extends StatelessWidget {
+  final IconData icon;
+
+  /// Overriddable color. Default is a fixed white.
+  final Color color;
+
+  /// Overriddable default size. This is just the initial hint, the actual size
+  /// is dynamic based on the widget's width (so that we show smaller icons in
+  /// smaller thumbnails).
+  final double baseSize;
+
+  const _BottomRightOverlayIcon(
+    this.icon, {
+    Key? key,
+    this.baseSize = 24,
+    this.color = Colors.white, // fixed
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        double inset = 4;
+        double size = baseSize;
+
+        if (constraints.hasBoundedWidth) {
+          final w = constraints.maxWidth;
+          if (w > 120) {
+            size = 24;
+          } else if (w < 75) {
+            inset = 3;
+            size = 16;
+          }
+        }
+
+        return Container(
+          decoration: const BoxDecoration(
+            gradient: LinearGradient(
+              begin: Alignment.bottomRight,
+              end: Alignment.center,
+              colors: [
+                Color.fromRGBO(0, 0, 0, 0.14),
+                Color.fromRGBO(0, 0, 0, 0.05),
+                Color.fromRGBO(0, 0, 0, 0.0),
+              ],
+              stops: [0, 0.6, 1],
+            ),
+          ),
+          child: Align(
+            alignment: Alignment.bottomRight,
+            child: Padding(
+              padding: EdgeInsets.only(bottom: inset, right: inset),
+              child: Icon(
+                icon,
+                size: size,
+                color: color,
+              ),
+            ),
+          ),
+        );
+      },
     );
     );
   }
   }
 }
 }

+ 49 - 8
lib/ui/viewer/file/file_info_widget.dart

@@ -1,5 +1,7 @@
 // @dart=2.9
 // @dart=2.9
 
 
+import 'dart:ui';
+
 import "package:exif/exif.dart";
 import "package:exif/exif.dart";
 import "package:flutter/cupertino.dart";
 import "package:flutter/cupertino.dart";
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
@@ -9,6 +11,7 @@ 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/services/collections_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/icon_button_widget.dart';
@@ -47,10 +50,12 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
   };
   };
 
 
   bool _isImage = false;
   bool _isImage = false;
+  int _currentUserID;
 
 
   @override
   @override
   void initState() {
   void initState() {
     debugPrint('file_info_dialog initState');
     debugPrint('file_info_dialog initState');
+    _currentUserID = Configuration.instance.getUserID();
     _isImage = widget.file.fileType == FileType.image ||
     _isImage = widget.file.fileType == FileType.image ||
         widget.file.fileType == FileType.livePhoto;
         widget.file.fileType == FileType.livePhoto;
     if (_isImage) {
     if (_isImage) {
@@ -69,6 +74,8 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final file = widget.file;
     final file = widget.file;
     final fileIsBackedup = file.uploadedFileID == null ? false : true;
     final fileIsBackedup = file.uploadedFileID == null ? false : true;
+    final bool isFileOwner =
+        file.ownerID == null || file.ownerID == _currentUserID;
     Future<Set<int>> allCollectionIDsOfFile;
     Future<Set<int>> allCollectionIDsOfFile;
     Future<Set<String>>
     Future<Set<String>>
         allDeviceFoldersOfFile; //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
         allDeviceFoldersOfFile; //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
@@ -94,12 +101,14 @@ 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
+      !widget.file.isUploaded ||
+              (!isFileOwner && (widget.file.caption?.isEmpty ?? true))
           ? const SizedBox.shrink()
           ? const SizedBox.shrink()
           : Padding(
           : Padding(
               padding: const EdgeInsets.only(top: 8, bottom: 4),
               padding: const EdgeInsets.only(top: 8, bottom: 4),
-              child: FileCaptionWidget(file: widget.file),
+              child: isFileOwner
+                  ? FileCaptionWidget(file: widget.file)
+                  : FileCaptionReadyOnly(caption: widget.file.caption),
             ),
             ),
       ListTile(
       ListTile(
         horizontalTitleGap: 2,
         horizontalTitleGap: 2,
@@ -122,8 +131,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               ),
               ),
         ),
         ),
         trailing: (widget.file.ownerID == null ||
         trailing: (widget.file.ownerID == null ||
-                    widget.file.ownerID ==
-                        Configuration.instance.getUserID()) &&
+                    widget.file.ownerID == _currentUserID) &&
                 widget.file.uploadedFileID != null
                 widget.file.uploadedFileID != null
             ? IconButton(
             ? IconButton(
                 onPressed: () {
                 onPressed: () {
@@ -170,8 +178,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                 : const SizedBox.shrink(),
                 : const SizedBox.shrink(),
           ],
           ],
         ),
         ),
-        trailing: file.uploadedFileID == null ||
-                file.ownerID != Configuration.instance.getUserID()
+        trailing: file.uploadedFileID == null || file.ownerID != _currentUserID
             ? const SizedBox.shrink()
             ? const SizedBox.shrink()
             : IconButton(
             : IconButton(
                 onPressed: () async {
                 onPressed: () async {
@@ -223,7 +230,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
           horizontalTitleGap: 0,
           horizontalTitleGap: 0,
           leading: const Icon(Icons.folder_outlined),
           leading: const Icon(Icons.folder_outlined),
           title: fileIsBackedup
           title: fileIsBackedup
-              ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
+              ? CollectionsListOfFileWidget(
+                  allCollectionIDsOfFile,
+                  _currentUserID,
+                )
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
         ),
         ),
       ),
       ),
@@ -282,6 +292,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                   onTap: () => Navigator.pop(context),
                   onTap: () => Navigator.pop(context),
                 ),
                 ),
               ),
               ),
+              SliverToBoxAdapter(child: addedBy(widget.file)),
               SliverList(
               SliverList(
                 delegate: SliverChildBuilderDelegate(
                 delegate: SliverChildBuilderDelegate(
                   (context, index) {
                   (context, index) {
@@ -305,6 +316,36 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
     );
     );
   }
   }
 
 
+  Widget addedBy(File file) {
+    if (file.uploadedFileID == null) {
+      return const SizedBox.shrink();
+    }
+    String addedBy;
+    if (file.ownerID == _currentUserID) {
+      if (file.pubMagicMetadata.uploaderName != null) {
+        addedBy = file.pubMagicMetadata.uploaderName;
+      }
+    } else {
+      final fileOwner = CollectionsService.instance
+          .getFileOwner(file.ownerID, file.collectionID);
+      if (fileOwner != null) {
+        addedBy = fileOwner.email;
+      }
+    }
+    if (addedBy == null || addedBy.isEmpty) {
+      return const SizedBox.shrink();
+    }
+    final enteTheme = Theme.of(context).colorScheme.enteTheme;
+    return Padding(
+      padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 16),
+      child: Text(
+        "Added by $addedBy",
+        style: enteTheme.textTheme.mini
+            .copyWith(color: enteTheme.colorScheme.textMuted),
+      ),
+    );
+  }
+
   _generateExifForDetails(Map<String, IfdTag> exif) {
   _generateExifForDetails(Map<String, IfdTag> exif) {
     if (exif["EXIF FocalLength"] != null) {
     if (exif["EXIF FocalLength"] != null) {
       _exifData["focalLength"] =
       _exifData["focalLength"] =

+ 30 - 0
lib/ui/viewer/file/thumbnail_widget.dart

@@ -3,6 +3,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/cache/thumbnail_cache.dart';
 import 'package:photos/core/cache/thumbnail_cache.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
@@ -10,9 +11,12 @@ import 'package:photos/db/files_db.dart';
 import 'package:photos/db/trash_db.dart';
 import 'package:photos/db/trash_db.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/extensions/string_ext.dart';
+import 'package:photos/models/collection.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/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/ui/viewer/file/file_icons_widget.dart';
 import 'package:photos/ui/viewer/file/file_icons_widget.dart';
 import 'package:photos/utils/file_util.dart';
 import 'package:photos/utils/file_util.dart';
@@ -28,6 +32,7 @@ class ThumbnailWidget extends StatefulWidget {
   final Duration diskLoadDeferDuration;
   final Duration diskLoadDeferDuration;
   final Duration serverLoadDeferDuration;
   final Duration serverLoadDeferDuration;
   final int thumbnailSize;
   final int thumbnailSize;
+  final bool shouldShowOwnerAvatar;
 
 
   ThumbnailWidget(
   ThumbnailWidget(
     this.file, {
     this.file, {
@@ -37,6 +42,7 @@ class ThumbnailWidget extends StatefulWidget {
     this.shouldShowLivePhotoOverlay = false,
     this.shouldShowLivePhotoOverlay = false,
     this.shouldShowArchiveStatus = false,
     this.shouldShowArchiveStatus = false,
     this.showFavForAlbumOnly = false,
     this.showFavForAlbumOnly = false,
+    this.shouldShowOwnerAvatar = false,
     this.diskLoadDeferDuration,
     this.diskLoadDeferDuration,
     this.serverLoadDeferDuration,
     this.serverLoadDeferDuration,
     this.thumbnailSize = thumbnailSmallSize,
     this.thumbnailSize = thumbnailSmallSize,
@@ -111,6 +117,30 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
           widget.shouldShowLivePhotoOverlay) {
           widget.shouldShowLivePhotoOverlay) {
         contentChildren.add(const LivePhotoOverlayIcon());
         contentChildren.add(const LivePhotoOverlayIcon());
       }
       }
+      if (widget.shouldShowOwnerAvatar) {
+        final owner = CollectionsService.instance
+            .getFileOwner(widget.file.ownerID, widget.file.collectionID);
+        if (widget.file.ownerID != null &&
+            widget.file.ownerID != Configuration.instance.getUserID()) {
+          // hide this icon if the current thumbnail is being showed as album
+          // cover
+          contentChildren.add(
+            OwnerAvatarOverlayIcon(owner),
+          );
+        } else if (widget.file.pubMagicMetadata.uploaderName != null) {
+          contentChildren.add(
+            // Use uploadName hashCode as userID so that different uploader
+            // get avatar color
+            OwnerAvatarOverlayIcon(
+              User(
+                id: widget.file.pubMagicMetadata.uploaderName.sumAsciiValues,
+                email: owner.email,
+                name: widget.file.pubMagicMetadata.uploaderName,
+              ),
+            ),
+          );
+        }
+      }
       content = contentChildren.length == 1
       content = contentChildren.length == 1
           ? contentChildren.first
           ? contentChildren.first
           : Stack(
           : Stack(

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

@@ -9,9 +9,9 @@ import 'package:photos/models/gallery_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/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery.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';
 
 
 class ArchivePage extends StatelessWidget {
 class ArchivePage extends StatelessWidget {
   final String tagPrefix;
   final String tagPrefix;
@@ -78,10 +78,10 @@ class ArchivePage extends StatelessWidget {
         alignment: Alignment.bottomCenter,
         alignment: Alignment.bottomCenter,
         children: [
         children: [
           gallery,
           gallery,
-          GalleryOverlayWidget(
+          FileSelectionOverlayBar(
             overlayType,
             overlayType,
             _selectedFiles,
             _selectedFiles,
-          )
+          ),
         ],
         ],
       ),
       ),
     );
     );

+ 49 - 17
lib/ui/viewer/gallery/collection_page.dart

@@ -1,6 +1,7 @@
 // @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/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/collection_updated_event.dart';
@@ -10,19 +11,18 @@ 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/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery.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';
 
 
-class CollectionPage extends StatelessWidget {
+class CollectionPage extends StatefulWidget {
   final CollectionWithThumbnail c;
   final CollectionWithThumbnail c;
   final String tagPrefix;
   final String tagPrefix;
   final GalleryType appBarType;
   final GalleryType appBarType;
-  final _selectedFiles = SelectedFiles();
   final bool hasVerifiedLock;
   final bool hasVerifiedLock;
 
 
-  CollectionPage(
+  const CollectionPage(
     this.c, {
     this.c, {
     this.tagPrefix = "collection",
     this.tagPrefix = "collection",
     this.appBarType = GalleryType.ownedCollection,
     this.appBarType = GalleryType.ownedCollection,
@@ -30,17 +30,43 @@ class CollectionPage extends StatelessWidget {
     Key key,
     Key key,
   }) : super(key: key);
   }) : super(key: key);
 
 
+  @override
+  State<CollectionPage> createState() => _CollectionPageState();
+}
+
+class _CollectionPageState extends State<CollectionPage> {
+  final _selectedFiles = SelectedFiles();
+
+  bool _isCollectionOwner = false;
+  final GlobalKey shareButtonKey = GlobalKey();
+  final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
+
+  @override
+  void initState() {
+    _selectedFiles.addListener(_selectedFilesListener);
+    _isCollectionOwner =
+        Configuration.instance.getUserID() == widget.c.collection.owner.id;
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _selectedFiles.removeListener(_selectedFilesListener);
+    super.dispose();
+  }
+
   @override
   @override
   Widget build(Object context) {
   Widget build(Object context) {
-    if (hasVerifiedLock == false && c.collection.isHidden()) {
+    if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) {
       return const EmptyState();
       return const EmptyState();
     }
     }
-    final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
+    final initialFiles =
+        widget.c.thumbnail != null ? [widget.c.thumbnail] : null;
     final gallery = Gallery(
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
         final FileLoadResult result =
         final FileLoadResult result =
             await FilesDB.instance.getFilesInCollection(
             await FilesDB.instance.getFilesInCollection(
-          c.collection.id,
+          widget.c.collection.id,
           creationStartTime,
           creationStartTime,
           creationEndTime,
           creationEndTime,
           limit: limit,
           limit: limit,
@@ -57,38 +83,44 @@ class CollectionPage extends StatelessWidget {
       },
       },
       reloadEvent: Bus.instance
       reloadEvent: Bus.instance
           .on<CollectionUpdatedEvent>()
           .on<CollectionUpdatedEvent>()
-          .where((event) => event.collectionID == c.collection.id),
+          .where((event) => event.collectionID == widget.c.collection.id),
       removalEventTypes: const {
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
         EventType.deletedFromEverywhere,
         EventType.hide,
         EventType.hide,
       },
       },
-      tagPrefix: tagPrefix,
+      tagPrefix: widget.tagPrefix,
       selectedFiles: _selectedFiles,
       selectedFiles: _selectedFiles,
       initialFiles: initialFiles,
       initialFiles: initialFiles,
-      albumName: c.collection.name,
+      albumName: widget.c.collection.name,
     );
     );
     return Scaffold(
     return Scaffold(
       appBar: PreferredSize(
       appBar: PreferredSize(
         preferredSize: const Size.fromHeight(50.0),
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
         child: GalleryAppBarWidget(
-          appBarType,
-          c.collection.name,
+          widget.appBarType,
+          widget.c.collection.name,
           _selectedFiles,
           _selectedFiles,
-          collection: c.collection,
+          collection: widget.c.collection,
         ),
         ),
       ),
       ),
       body: Stack(
       body: Stack(
         alignment: Alignment.bottomCenter,
         alignment: Alignment.bottomCenter,
         children: [
         children: [
           gallery,
           gallery,
-          GalleryOverlayWidget(
-            appBarType,
+          FileSelectionOverlayBar(
+            widget.appBarType,
             _selectedFiles,
             _selectedFiles,
-            collection: c.collection,
-          ),
+            collection: widget.c.collection,
+          )
         ],
         ],
       ),
       ),
     );
     );
   }
   }
+
+  _selectedFilesListener() {
+    _selectedFiles.files.isNotEmpty
+        ? _bottomPosition.value = 0.0
+        : _bottomPosition.value = -150.0;
+  }
 }
 }

+ 2 - 2
lib/ui/viewer/gallery/device_folder_page.dart

@@ -12,9 +12,9 @@ import 'package:photos/models/device_collection.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/remote_sync_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
+import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery.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';
 
 
 class DeviceFolderPage extends StatelessWidget {
 class DeviceFolderPage extends StatelessWidget {
   final DeviceCollection deviceCollection;
   final DeviceCollection deviceCollection;
@@ -61,7 +61,7 @@ class DeviceFolderPage extends StatelessWidget {
         alignment: Alignment.bottomCenter,
         alignment: Alignment.bottomCenter,
         children: [
         children: [
           gallery,
           gallery,
-          GalleryOverlayWidget(
+          FileSelectionOverlayBar(
             GalleryType.localFolder,
             GalleryType.localFolder,
             _selectedFiles,
             _selectedFiles,
           )
           )

+ 61 - 58
lib/ui/viewer/gallery/empty_hidden_widget.dart

@@ -12,68 +12,71 @@ class EmptyHiddenWidget extends StatelessWidget {
     final EnteColorScheme enteColorScheme = getEnteColorScheme(context);
     final EnteColorScheme enteColorScheme = getEnteColorScheme(context);
     return Padding(
     return Padding(
       padding: const EdgeInsets.all(8.0),
       padding: const EdgeInsets.all(8.0),
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Icon(
-            Icons.visibility_off,
-            color: enteColorScheme.strokeMuted,
-            size: 24,
-          ),
-          const SizedBox(height: 10),
-          Text(
-            "No hidden photos or videos",
-            textAlign: TextAlign.center,
-            style: enteTextTheme.body.copyWith(
-              color: enteColorScheme.textMuted,
+      child: SizedBox(
+        width: double.infinity,
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Icon(
+              Icons.visibility_off,
+              color: enteColorScheme.strokeMuted,
+              size: 24,
             ),
             ),
-          ),
-          const SizedBox(height: 36),
-          Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              const EmptyHiddenTextWidget("To hide a photo or video"),
-              const SizedBox(height: 4),
-              Padding(
-                padding: const EdgeInsets.only(left: 6),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    const EmptyHiddenTextWidget("• Open the item"),
-                    const SizedBox(height: 2),
-                    const EmptyHiddenTextWidget(
-                      "• Click on the overflow menu",
-                    ),
-                    const SizedBox(height: 2),
-                    SizedBox(
-                      width: 120,
-                      child: Row(
-                        children: [
-                          const EmptyHiddenTextWidget("• Click "),
-                          const SizedBox(width: 4),
-                          Icon(
-                            Icons.visibility_off,
-                            color: enteColorScheme.strokeBase,
-                            size: 16,
-                          ),
-                          const Padding(
-                            padding: EdgeInsets.all(4),
-                          ),
-                          Text(
-                            "Hide",
-                            style: TextStyle(
-                              color: enteColorScheme.textBase,
+            const SizedBox(height: 10),
+            Text(
+              "No hidden photos or videos",
+              textAlign: TextAlign.center,
+              style: enteTextTheme.body.copyWith(
+                color: enteColorScheme.textMuted,
+              ),
+            ),
+            const SizedBox(height: 36),
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                const EmptyHiddenTextWidget("To hide a photo or video"),
+                const SizedBox(height: 4),
+                Padding(
+                  padding: const EdgeInsets.only(left: 6),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      const EmptyHiddenTextWidget("• Open the item"),
+                      const SizedBox(height: 2),
+                      const EmptyHiddenTextWidget(
+                        "• Click on the overflow menu",
+                      ),
+                      const SizedBox(height: 2),
+                      SizedBox(
+                        width: 120,
+                        child: Row(
+                          children: [
+                            const EmptyHiddenTextWidget("• Click "),
+                            const SizedBox(width: 4),
+                            Icon(
+                              Icons.visibility_off,
+                              color: enteColorScheme.strokeBase,
+                              size: 16,
+                            ),
+                            const Padding(
+                              padding: EdgeInsets.all(4),
                             ),
                             ),
-                          ),
-                        ],
+                            Text(
+                              "Hide",
+                              style: TextStyle(
+                                color: enteColorScheme.textBase,
+                              ),
+                            ),
+                          ],
+                        ),
                       ),
                       ),
-                    ),
-                  ],
+                    ],
+                  ),
                 ),
                 ),
-              ),
-            ],
-          ),
-        ],
+              ],
+            ),
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }

+ 23 - 20
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -21,7 +21,8 @@ import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/rename_dialog.dart';
 import 'package:photos/ui/common/rename_dialog.dart';
-import 'package:photos/ui/sharing/share_collection_widget.dart';
+import 'package:photos/ui/sharing/album_participants_page.dart';
+import 'package:photos/ui/sharing/share_collection_page.dart';
 import 'package:photos/ui/tools/free_space_page.dart';
 import 'package:photos/ui/tools/free_space_page.dart';
 import 'package:photos/utils/data_util.dart';
 import 'package:photos/utils/data_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -249,7 +250,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     final List<Widget> actions = <Widget>[];
     final List<Widget> actions = <Widget>[];
     if (Configuration.instance.hasConfiguredAccount() &&
     if (Configuration.instance.hasConfiguredAccount() &&
         widget.selectedFiles.files.isEmpty &&
         widget.selectedFiles.files.isEmpty &&
-        widget.type == GalleryType.ownedCollection &&
+        (widget.type == GalleryType.ownedCollection ||
+            widget.type == GalleryType.sharedCollection) &&
         widget.collection?.type != CollectionType.favorites) {
         widget.collection?.type != CollectionType.favorites) {
       actions.add(
       actions.add(
         Tooltip(
         Tooltip(
@@ -426,31 +428,32 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
   }
   }
 
 
   Future<void> _showShareCollectionDialog() async {
   Future<void> _showShareCollectionDialog() async {
-    var collection = widget.collection;
-    final dialog = createProgressDialog(context, "Please wait...");
-    await dialog.show();
+    final collection = widget.collection;
     try {
     try {
-      if (collection == null || widget.type != GalleryType.ownedCollection) {
+      if (collection == null ||
+          (widget.type != GalleryType.ownedCollection &&
+              widget.type != GalleryType.sharedCollection)) {
         throw Exception(
         throw Exception(
-          "Cannot share empty collection of type ${widget.type}",
+          "Cannot share empty collection of typex ${widget.type}",
+        );
+      }
+      if (Configuration.instance.getUserID() == widget.collection.owner.id) {
+        unawaited(
+          routeToPage(
+            context,
+            ShareCollectionPage(collection),
+          ),
         );
         );
       } else {
       } else {
-        final sharees =
-            await CollectionsService.instance.getSharees(collection.id);
-        collection = collection.copyWith(sharees: sharees);
+        unawaited(
+          routeToPage(
+            context,
+            AlbumParticipantsPage(collection),
+          ),
+        );
       }
       }
-      await dialog.hide();
-      return showDialog<void>(
-        context: context,
-        builder: (BuildContext context) {
-          return SharingDialog(
-            collection,
-          );
-        },
-      );
     } catch (e, s) {
     } catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
-      await dialog.hide();
       showGenericErrorDialog(context);
       showGenericErrorDialog(context);
     }
     }
   }
   }

+ 3 - 3
lib/ui/viewer/gallery/hidden_page.dart

@@ -8,10 +8,10 @@ import 'package:photos/events/files_updated_event.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/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/empty_hidden_widget.dart';
 import 'package:photos/ui/viewer/gallery/empty_hidden_widget.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';
 
 
 class HiddenPage extends StatelessWidget {
 class HiddenPage extends StatelessWidget {
   final String tagPrefix;
   final String tagPrefix;
@@ -80,10 +80,10 @@ class HiddenPage extends StatelessWidget {
         alignment: Alignment.bottomCenter,
         alignment: Alignment.bottomCenter,
         children: [
         children: [
           gallery,
           gallery,
-          GalleryOverlayWidget(
+          FileSelectionOverlayBar(
             overlayType,
             overlayType,
             _selectedFiles,
             _selectedFiles,
-          )
+          ),
         ],
         ],
       ),
       ),
     );
     );

+ 3 - 3
lib/ui/viewer/search/result/search_result_page.dart

@@ -9,9 +9,9 @@ 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/search/search_result.dart';
 import 'package:photos/models/search/search_result.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/selected_files.dart';
+import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery.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';
 
 
 class SearchResultPage extends StatelessWidget {
 class SearchResultPage extends StatelessWidget {
   final SearchResult searchResult;
   final SearchResult searchResult;
@@ -51,7 +51,7 @@ class SearchResultPage extends StatelessWidget {
       },
       },
       tagPrefix: searchResult.heroTag(),
       tagPrefix: searchResult.heroTag(),
       selectedFiles: _selectedFiles,
       selectedFiles: _selectedFiles,
-      initialFiles: [],
+      initialFiles: const [],
     );
     );
     return Scaffold(
     return Scaffold(
       appBar: PreferredSize(
       appBar: PreferredSize(
@@ -66,7 +66,7 @@ class SearchResultPage extends StatelessWidget {
         alignment: Alignment.bottomCenter,
         alignment: Alignment.bottomCenter,
         children: [
         children: [
           gallery,
           gallery,
-          GalleryOverlayWidget(
+          FileSelectionOverlayBar(
             overlayType,
             overlayType,
             _selectedFiles,
             _selectedFiles,
           )
           )

+ 99 - 0
lib/utils/delete_file_util.dart

@@ -6,6 +6,7 @@ import 'dart:io';
 import 'dart:math';
 import 'dart:math';
 
 
 import 'package:device_info/device_info.dart';
 import 'package:device_info/device_info.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photo_manager/photo_manager.dart';
@@ -16,6 +17,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/local_photos_updated_event.dart';
 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/selected_files.dart';
 import 'package:photos/models/trash_item_request.dart';
 import 'package:photos/models/trash_item_request.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/sync_service.dart';
@@ -485,3 +487,100 @@ Future<bool> shouldProceedWithDeletion(BuildContext context) async {
   );
   );
   return choice == DialogUserChoice.secondChoice;
   return choice == DialogUserChoice.secondChoice;
 }
 }
+
+void showDeleteSheet(BuildContext context, SelectedFiles selectedFiles) {
+  final count = selectedFiles.files.length;
+  bool containsUploadedFile = false, containsLocalFile = false;
+  for (final file in selectedFiles.files) {
+    if (file.uploadedFileID != null) {
+      containsUploadedFile = true;
+    }
+    if (file.localID != null) {
+      containsLocalFile = true;
+    }
+  }
+  final actions = <Widget>[];
+  if (containsUploadedFile && containsLocalFile) {
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesOnDeviceOnly(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+          showToast(context, "Files deleted from device");
+        },
+        child: const Text("Device"),
+      ),
+    );
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromRemoteOnly(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+
+          showShortToast(context, "Moved to trash");
+        },
+        child: const Text("ente"),
+      ),
+    );
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromEverywhere(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+        },
+        child: const Text("Everywhere"),
+      ),
+    );
+  } else {
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromEverywhere(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+        },
+        child: const Text("Delete"),
+      ),
+    );
+  }
+  final action = CupertinoActionSheet(
+    title: Text(
+      "Delete " +
+          count.toString() +
+          " file" +
+          (count == 1 ? "" : "s") +
+          (containsUploadedFile && containsLocalFile ? " from" : "?"),
+    ),
+    actions: actions,
+    cancelButton: CupertinoActionSheetAction(
+      child: const Text("Cancel"),
+      onPressed: () {
+        Navigator.of(context, rootNavigator: true).pop();
+      },
+    ),
+  );
+  showCupertinoModalPopup(
+    context: context,
+    builder: (_) => action,
+    barrierColor: Colors.black.withOpacity(0.75),
+  );
+}

+ 6 - 2
lib/utils/dialog_util.dart

@@ -7,11 +7,15 @@ import 'package:flutter/material.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 
 
-ProgressDialog createProgressDialog(BuildContext context, String message) {
+ProgressDialog createProgressDialog(
+  BuildContext context,
+  String message, {
+  isDismissible = false,
+}) {
   final dialog = ProgressDialog(
   final dialog = ProgressDialog(
     context,
     context,
     type: ProgressDialogType.normal,
     type: ProgressDialogType.normal,
-    isDismissible: false,
+    isDismissible: isDismissible,
     barrierColor: Colors.black12,
     barrierColor: Colors.black12,
   );
   );
   dialog.style(
   dialog.style(

+ 3 - 2
lib/utils/email_util.dart

@@ -199,7 +199,7 @@ Future<void> sendEmail(
     );
     );
     if (Platform.isAndroid) {
     if (Platform.isAndroid) {
       // Special handling due to issue in proton mail android client
       // Special handling due to issue in proton mail android client
-      // https://github.com/ente-io/frame/pull/253
+      // https://github.com/ente-io/photos-app/pull/253
       final Uri params = Uri(
       final Uri params = Uri(
         scheme: 'mailto',
         scheme: 'mailto',
         path: to,
         path: to,
@@ -212,7 +212,8 @@ Future<void> sendEmail(
         throw Exception('Could not launch ${params.toString()}');
         throw Exception('Could not launch ${params.toString()}');
       }
       }
     } else {
     } else {
-      final OpenMailAppResult result = await OpenMailApp.composeNewEmailInMailApp(
+      final OpenMailAppResult result =
+          await OpenMailApp.composeNewEmailInMailApp(
         nativePickerTitle: 'Select email app',
         nativePickerTitle: 'Select email app',
         emailContent: email,
         emailContent: email,
       );
       );

+ 13 - 0
lib/utils/separators_util.dart

@@ -0,0 +1,13 @@
+import 'package:flutter/material.dart';
+
+//This method returns a newly declared list with separators. It will not
+//modify the original list
+List<Widget> addSeparators(List<Widget> listOfWidgets, Widget separator) {
+  final int initialLength = listOfWidgets.length;
+  final listOfWidgetsWithSeparators = <Widget>[];
+  listOfWidgetsWithSeparators.addAll(listOfWidgets);
+  for (var i = 1; i < initialLength; i++) {
+    listOfWidgetsWithSeparators.insert((2 * i) - 1, separator);
+  }
+  return listOfWidgetsWithSeparators;
+}

+ 18 - 1
lib/utils/share_util.dart

@@ -24,7 +24,12 @@ Future<void> share(
   List<File> files, {
   List<File> files, {
   GlobalKey shareButtonKey,
   GlobalKey shareButtonKey,
 }) async {
 }) async {
-  final dialog = createProgressDialog(context, "Preparing...");
+  final remoteFileCount = files.where((element) => element.isRemoteFile).length;
+  final dialog = createProgressDialog(
+    context,
+    "Preparing...",
+    isDismissible: remoteFileCount > 2,
+  );
   await dialog.show();
   await dialog.show();
   try {
   try {
     final List<Future<String>> pathFutures = [];
     final List<Future<String>> pathFutures = [];
@@ -146,3 +151,15 @@ DateTime parseDateFromFileNam1e(String fileName) {
     );
     );
   }
   }
 }
 }
+
+void shareSelected(
+  BuildContext context,
+  GlobalKey shareButtonKey,
+  Set<File> selectedFiles,
+) {
+  share(
+    context,
+    selectedFiles.toList(),
+    shareButtonKey: shareButtonKey,
+  );
+}

+ 1 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
 
-version: 0.6.74+394
+version: 0.7.0+400
 
 
 environment:
 environment:
   sdk: '>=2.17.0 <3.0.0'
   sdk: '>=2.17.0 <3.0.0'

+ 1 - 0
test/utils/date_time_util_test.dart

@@ -17,6 +17,7 @@ void main() {
       "IMG_20210921_144423_783",
       "IMG_20210921_144423_783",
       "Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
       "Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
       "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
       "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
+      "signal-2022-12-17-15-16-04-718.jpg"
     ];
     ];
     for (String val in validParsing) {
     for (String val in validParsing) {
       final parsedValue = parseDateTimeFromFileNameV2(val);
       final parsedValue = parseDateTimeFromFileNameV2(val);