Browse Source

Resolved merge conflicts

ashilkn 2 years ago
parent
commit
47a3221484
82 changed files with 1495 additions and 1020 deletions
  1. 1 1
      README.md
  2. 5 5
      ios/Podfile.lock
  3. 2 2
      ios/Runner.xcodeproj/project.pbxproj
  4. 4 2
      lib/app.dart
  5. 5 0
      lib/events/sync_status_update_event.dart
  6. 15 4
      lib/main.dart
  7. 1 1
      lib/models/search/button_result.dart
  8. 6 1
      lib/models/user_details.dart
  9. 1 0
      lib/services/collections_service.dart
  10. 10 1
      lib/services/feature_flag_service.dart
  11. 45 30
      lib/services/remote_sync_service.dart
  12. 166 1
      lib/services/user_service.dart
  13. 22 1
      lib/ui/account/delete_account_page.dart
  14. 1 1
      lib/ui/account/password_reentry_page.dart
  15. 1 6
      lib/ui/account/two_factor_authentication_page.dart
  16. 110 0
      lib/ui/account/two_factor_recovery_page.dart
  17. 1 1
      lib/ui/account/verify_recovery_page.dart
  18. 1 1
      lib/ui/actions/collection/collection_file_actions.dart
  19. 1 1
      lib/ui/actions/collection/collection_sharing_actions.dart
  20. 3 3
      lib/ui/actions/file/file_actions.dart
  21. 1 1
      lib/ui/advanced_settings_screen.dart
  22. 1 1
      lib/ui/backup_settings_screen.dart
  23. 35 36
      lib/ui/collection_action_sheet.dart
  24. 1 1
      lib/ui/collections_gallery_widget.dart
  25. 14 6
      lib/ui/common/loading_widget.dart
  26. 1 1
      lib/ui/components/action_sheet_widget.dart
  27. 1 1
      lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart
  28. 1 1
      lib/ui/components/buttons/button_widget.dart
  29. 54 0
      lib/ui/components/buttons/chip_button_widget.dart
  30. 4 3
      lib/ui/components/buttons/icon_button_widget.dart
  31. 20 0
      lib/ui/components/buttons/inline_button_widget.dart
  32. 1 1
      lib/ui/components/dialog_widget.dart
  33. 8 1
      lib/ui/components/divider_widget.dart
  34. 1 1
      lib/ui/components/home_header_widget.dart
  35. 99 0
      lib/ui/components/info_item_widget.dart
  36. 1 1
      lib/ui/components/models/button_type.dart
  37. 1 1
      lib/ui/components/notification_widget.dart
  38. 5 3
      lib/ui/components/text_input_widget.dart
  39. 1 1
      lib/ui/components/title_bar_widget.dart
  40. 2 2
      lib/ui/growth/apply_code_screen.dart
  41. 1 1
      lib/ui/growth/code_success_screen.dart
  42. 1 1
      lib/ui/growth/referral_screen.dart
  43. 1 1
      lib/ui/growth/storage_details_screen.dart
  44. 1 1
      lib/ui/home/landing_page_widget.dart
  45. 9 13
      lib/ui/home/memories_widget.dart
  46. 4 0
      lib/ui/home/status_bar_widget.dart
  47. 1 1
      lib/ui/new_shared_collections_gallery.dart
  48. 1 1
      lib/ui/notification/update/change_log_page.dart
  49. 1 1
      lib/ui/payment/child_subscription_widget.dart
  50. 1 1
      lib/ui/payment/stripe_subscription_page.dart
  51. 1 1
      lib/ui/sharing/add_partipant_page.dart
  52. 1 1
      lib/ui/sharing/manage_album_participant.dart
  53. 1 1
      lib/ui/sharing/verify_identity_dialog.dart
  54. 3 1
      lib/ui/tools/app_lock.dart
  55. 1 1
      lib/ui/tools/debug/app_storage_viewer.dart
  56. 1 1
      lib/ui/tools/editor/image_editor_page.dart
  57. 1 1
      lib/ui/viewer/actions/delete_empty_albums.dart
  58. 1 1
      lib/ui/viewer/actions/file_selection_actions_widget.dart
  59. 1 1
      lib/ui/viewer/actions/file_selection_overlay_bar.dart
  60. 0 68
      lib/ui/viewer/file/collections_list_of_file_widget.dart
  61. 0 37
      lib/ui/viewer/file/device_folders_list_of_file_widget.dart
  62. 17 14
      lib/ui/viewer/file/exif_info_dialog.dart
  63. 255 0
      lib/ui/viewer/file/file_details_widget.dart
  64. 0 42
      lib/ui/viewer/file/file_info_collection_widget.dart
  65. 0 536
      lib/ui/viewer/file/file_info_widget.dart
  66. 0 77
      lib/ui/viewer/file/object_tags_widget.dart
  67. 0 69
      lib/ui/viewer/file/raw_exif_list_tile_widget.dart
  68. 37 0
      lib/ui/viewer/file_details/added_by_widget.dart
  69. 109 0
      lib/ui/viewer/file_details/albums_item_widget.dart
  70. 31 0
      lib/ui/viewer/file_details/backed_up_time_item_widget.dart
  71. 76 0
      lib/ui/viewer/file_details/creation_time_item_widget.dart
  72. 96 0
      lib/ui/viewer/file_details/exif_item_widgets.dart
  73. 99 0
      lib/ui/viewer/file_details/file_properties_item_widget.dart
  74. 49 0
      lib/ui/viewer/file_details/objects_item_widget.dart
  75. 1 1
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  76. 1 1
      lib/ui/viewer/search/search_widget.dart
  77. 0 14
      lib/utils/auth_util.dart
  78. 1 1
      lib/utils/delete_file_util.dart
  79. 1 1
      lib/utils/dialog_util.dart
  80. 1 1
      lib/utils/email_util.dart
  81. 34 2
      pubspec.lock
  82. 2 2
      pubspec.yaml

+ 1 - 1
README.md

@@ -66,7 +66,7 @@ You can alternatively install the build from PlayStore or F-Droid.
 3. Pull in all submodules with `git submodule update --init --recursive`
 4. Enable repo git hooks `git config core.hooksPath hooks`
 5. Setup TensorFlowLite by executing `setup.sh`
-6. For Android, run `flutter build apk --release --flavor independent`
+6. For Android, [setup your keystore](https://docs.flutter.dev/deployment/android#create-an-upload-keystore) and run `flutter build apk --release --flavor independent`
 7. For iOS, run `flutter build ios`
 
 <br/>

+ 5 - 5
ios/Podfile.lock

@@ -108,7 +108,7 @@ PODS:
   - libwebp/mux (1.2.4):
     - libwebp/demux
   - libwebp/webp (1.2.4)
-  - local_auth (0.0.1):
+  - local_auth_ios (0.0.1):
     - Flutter
   - Mantle (2.2.0):
     - Mantle/extobjc (= 2.2.0)
@@ -195,7 +195,7 @@ DEPENDENCIES:
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`)
   - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
-  - local_auth (from `.symlinks/plugins/local_auth/ios`)
+  - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
   - media_extension (from `.symlinks/plugins/media_extension/ios`)
   - motionphoto (from `.symlinks/plugins/motionphoto/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
@@ -276,8 +276,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/image_editor_common/ios"
   in_app_purchase_storekit:
     :path: ".symlinks/plugins/in_app_purchase_storekit/ios"
-  local_auth:
-    :path: ".symlinks/plugins/local_auth/ios"
+  local_auth_ios:
+    :path: ".symlinks/plugins/local_auth_ios/ios"
   media_extension:
     :path: ".symlinks/plugins/media_extension/ios"
   motionphoto:
@@ -346,7 +346,7 @@ SPEC CHECKSUMS:
   image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
   in_app_purchase_storekit: 6b297e2b5eab9fa3251a492d57301722e4132a71
   libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
-  local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
+  local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
   Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
   media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c
   motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16

+ 2 - 2
ios/Runner.xcodeproj/project.pbxproj

@@ -290,7 +290,7 @@
 				"${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework",
 				"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
 				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
-				"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
+				"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
 				"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
 				"${BUILT_PRODUCTS_DIR}/motionphoto/motionphoto.framework",
 				"${BUILT_PRODUCTS_DIR}/move_to_background/move_to_background.framework",
@@ -346,7 +346,7 @@
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
-				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motionphoto.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/move_to_background.framework",

+ 4 - 2
lib/app.dart

@@ -18,10 +18,12 @@ import "package:photos/utils/intent_util.dart";
 class EnteApp extends StatefulWidget {
   final Future<void> Function(String) runBackgroundTask;
   final Future<void> Function(String) killBackgroundTask;
+  final AdaptiveThemeMode? savedThemeMode;
 
   const EnteApp(
     this.runBackgroundTask,
-    this.killBackgroundTask, {
+    this.killBackgroundTask,
+    this.savedThemeMode, {
     Key? key,
   }) : super(key: key);
 
@@ -56,7 +58,7 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
       return AdaptiveTheme(
         light: lightThemeData,
         dark: darkThemeData,
-        initial: AdaptiveThemeMode.system,
+        initial: widget.savedThemeMode ?? AdaptiveThemeMode.system,
         builder: (lightTheme, dartTheme) => MaterialApp(
           title: "ente",
           themeMode: ThemeMode.system,

+ 5 - 0
lib/events/sync_status_update_event.dart

@@ -1,6 +1,10 @@
+import "package:logging/logging.dart";
+
 import 'package:photos/events/event.dart';
 
 class SyncStatusUpdate extends Event {
+  static final _logger = Logger("SyncStatusUpdate");
+
   final SyncStatus status;
   final int? completed;
   final int? total;
@@ -18,6 +22,7 @@ class SyncStatusUpdate extends Event {
     this.reason = "",
     this.error,
   }) {
+    _logger.info("Creating sync status: " + status.toString());
     timestamp = DateTime.now().microsecondsSinceEpoch;
   }
 }

+ 15 - 4
lib/main.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import "package:adaptive_theme/adaptive_theme.dart";
 import 'package:background_fetch/background_fetch.dart';
 import 'package:firebase_messaging/firebase_messaging.dart';
 import 'package:flutter/foundation.dart';
@@ -58,28 +59,38 @@ const kBackgroundLockLatency = Duration(seconds: 3);
 void main() async {
   debugRepaintRainbowEnabled = false;
   WidgetsFlutterBinding.ensureInitialized();
-  await _runInForeground();
+  final savedThemeMode = await AdaptiveTheme.getThemeMode();
+  await _runInForeground(savedThemeMode);
   BackgroundFetch.registerHeadlessTask(_headlessTaskHandler);
 }
 
-Future<void> _runInForeground() async {
+Future<void> _runInForeground(AdaptiveThemeMode? savedThemeMode) async {
   return await _runWithLogs(() async {
     _logger.info("Starting app in foreground");
     await _init(false, via: 'mainMethod');
     unawaited(_scheduleFGSync('appStart in FG'));
     runApp(
       AppLock(
-        builder: (args) => const EnteApp(_runBackgroundTask, _killBGTask),
+        builder: (args) =>
+            EnteApp(_runBackgroundTask, _killBGTask, savedThemeMode),
         lockScreen: const LockScreen(),
         enabled: Configuration.instance.shouldShowLockScreen(),
         lightTheme: lightThemeData,
         darkTheme: darkThemeData,
         backgroundLockLatency: kBackgroundLockLatency,
+        savedThemeMode: _themeMode(savedThemeMode),
       ),
     );
   });
 }
 
+ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) {
+  if (savedThemeMode == null) return ThemeMode.system;
+  if (savedThemeMode.isLight) return ThemeMode.light;
+  if (savedThemeMode.isDark) return ThemeMode.dark;
+  return ThemeMode.system;
+}
+
 Future<void> _runBackgroundTask(String taskId, {String mode = 'normal'}) async {
   if (_isProcessRunning) {
     _logger.info("Background task triggered when process was already running");
@@ -119,7 +130,7 @@ Future<void> _runInBackground(String taskId) async {
 // https://stackoverflow.com/a/73796478/546896
 @pragma('vm:entry-point')
 void _headlessTaskHandler(HeadlessTask task) {
-  print("_headlessTaskHandler");
+  debugPrint("_headlessTaskHandler");
   if (task.timeout) {
     BackgroundFetch.finish(task.taskId);
   } else {

+ 1 - 1
lib/models/search/button_result.dart

@@ -1,4 +1,4 @@
-import "package:photos/ui/components/button_widget.dart";
+import 'package:photos/ui/components/buttons/button_widget.dart';
 
 class ButtonResult {
   ///action can be null when action for the button that is returned when popping

+ 6 - 1
lib/models/user_details.dart

@@ -9,6 +9,7 @@ class UserDetails {
   final String email;
   final int usage;
   final int fileCount;
+  final int storageBonus;
   final int sharedCollectionsCount;
   final Subscription subscription;
   final FamilyData? familyData;
@@ -17,6 +18,7 @@ class UserDetails {
     this.email,
     this.usage,
     this.fileCount,
+    this.storageBonus,
     this.sharedCollectionsCount,
     this.subscription,
     this.familyData,
@@ -50,7 +52,8 @@ class UserDetails {
   }
 
   int getTotalStorage() {
-    return isPartOfFamily() ? familyData!.storage : subscription.storage;
+    return (isPartOfFamily() ? familyData!.storage : subscription.storage) +
+        storageBonus;
   }
 
   factory UserDetails.fromMap(Map<String, dynamic> map) {
@@ -58,6 +61,7 @@ class UserDetails {
       map['email'] as String,
       map['usage'] as int,
       (map['fileCount'] ?? 0) as int,
+      (map['storageBonus'] ?? 0) as int,
       (map['sharedCollectionsCount'] ?? 0) as int,
       Subscription.fromMap(map['subscription']),
       FamilyData.fromMap(map['familyData']),
@@ -69,6 +73,7 @@ class UserDetails {
       'email': email,
       'usage': usage,
       'fileCount': fileCount,
+      'storageBonus': storageBonus,
       'sharedCollectionsCount': sharedCollectionsCount,
       'subscription': subscription.toMap(),
       'familyData': familyData?.toMap(),

+ 1 - 0
lib/services/collections_service.dart

@@ -141,6 +141,7 @@ class CollectionsService {
     for (final collection in collections) {
       _cacheCollectionAttributes(collection);
     }
+    _logger.info("Collections synced");
     watch.log("collection cache refresh");
     if (fetchedCollections.isNotEmpty) {
       Bus.instance.fire(

+ 10 - 1
lib/services/feature_flag_service.dart

@@ -14,6 +14,12 @@ class FeatureFlagService {
   static final FeatureFlagService instance =
       FeatureFlagService._privateConstructor();
   static const _featureFlagsKey = "feature_flags_key";
+  static final _internalUserIDs = const String.fromEnvironment(
+    "internal_user_ids",
+    defaultValue: "1,2,3,4,191",
+  ).split(",").map((element) {
+    return int.parse(element);
+  }).toSet();
 
   final _logger = Logger("FeatureFlagService");
   FeatureFlags? _featureFlags;
@@ -64,7 +70,10 @@ class FeatureFlagService {
 
   bool isInternalUserOrDebugBuild() {
     final String? email = Configuration.instance.getEmail();
-    return (email != null && email.endsWith("@ente.io")) || kDebugMode;
+    final userID = Configuration.instance.getUserID();
+    return (email != null && email.endsWith("@ente.io")) ||
+        _internalUserIDs.contains(userID) ||
+        kDebugMode;
   }
 
   Future<void> fetchFeatureFlags() async {

+ 45 - 30
lib/services/remote_sync_service.dart

@@ -23,6 +23,7 @@ import 'package:photos/models/file_type.dart';
 import 'package:photos/models/upload_strategy.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
 import 'package:photos/services/collections_service.dart';
+import "package:photos/services/feature_flag_service.dart";
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_file_update_service.dart';
 import 'package:photos/services/sync_service.dart';
@@ -44,7 +45,7 @@ class RemoteSyncService {
   int _completedUploads = 0;
   late SharedPreferences _prefs;
   Completer<void>? _existingSync;
-  bool _existingSyncSilent = false;
+  bool _isExistingSyncSilent = false;
 
   static const kHasSyncedArchiveKey = "has_synced_archive";
   final String _isFirstRemoteSyncDone = "isFirstRemoteSyncDone";
@@ -84,13 +85,17 @@ class RemoteSyncService {
       _logger.info("Remote sync already in progress, skipping");
       // if current sync is silent but request sync is non-silent (demands UI
       // updates), update the syncSilently flag
-      if (_existingSyncSilent == true && silently == false) {
-        _existingSyncSilent = false;
+      if (_isExistingSyncSilent && !silently) {
+        _isExistingSyncSilent = false;
       }
       return _existingSync?.future;
     }
     _existingSync = Completer<void>();
-    _existingSyncSilent = silently;
+    _isExistingSyncSilent = silently;
+    _logger.info(
+      "Starting remote sync " +
+          (silently ? "silently" : " with status updates"),
+    );
 
     try {
       // use flag to decide if we should start marking files for upload before
@@ -115,18 +120,20 @@ class RemoteSyncService {
       }
       final filesToBeUploaded = await _getFilesToBeUploaded();
       final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
+      _logger.info("File upload complete");
       if (hasUploadedFiles) {
         await _pullDiff();
         _existingSync?.complete();
         _existingSync = null;
         await syncDeviceCollectionFilesForUpload();
         final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty;
+        _logger.info("hasMoreFilesToBackup?" + hasMoreFilesToBackup.toString());
         if (hasMoreFilesToBackup && !_shouldThrottleSync()) {
           // Skipping a resync to ensure that files that were ignored in this
           // session are not processed now
           sync();
         } else {
-          debugPrint("Fire backup completed event");
+          _logger.info("Fire backup completed event");
           Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
         }
       } else {
@@ -146,13 +153,17 @@ class RemoteSyncService {
         rethrow;
       } else {
         _logger.severe("Error executing remote sync ", e, s);
+        if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
+          rethrow;
+        }
       }
     } finally {
-      _existingSyncSilent = false;
+      _isExistingSyncSilent = false;
     }
   }
 
   Future<void> _pullDiff() async {
+    _logger.info("Pulling remote diff");
     final isFirstSync = !_collectionsService.hasSyncedCollections();
     await _collectionsService.sync();
     // check and reset user's collection syncTime in past for older clients
@@ -179,6 +190,7 @@ class RemoteSyncService {
       );
       await _collectionsService.setCollectionSyncTime(c.id, c.updationTime);
     }
+    _logger.info("All updated collections synced");
   }
 
   Future<void> _resetAllCollectionsSyncTime() async {
@@ -193,7 +205,12 @@ class RemoteSyncService {
   }
 
   Future<void> _syncCollectionDiff(int collectionID, int sinceTime) async {
-    if (!_existingSyncSilent) {
+    _logger.info(
+      "Syncing collection #" +
+          collectionID.toString() +
+          (_isExistingSyncSilent ? " silently" : ""),
+    );
+    if (!_isExistingSyncSilent) {
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
     }
     final diff =
@@ -252,10 +269,13 @@ class RemoteSyncService {
         collectionID,
         _collectionsService.getCollectionSyncTime(collectionID),
       );
+    } else {
+      _logger.info("Collection #" + collectionID.toString() + " synced");
     }
   }
 
   Future<void> syncDeviceCollectionFilesForUpload() async {
+    _logger.info("Syncing device collections to be uploaded");
     final int ownerID = _config.getUserID()!;
 
     final deviceCollections = await _db.getDeviceCollections();
@@ -279,15 +299,15 @@ class RemoteSyncService {
       if (localIDsToSync.isEmpty) {
         continue;
       }
-      await _createCollectionForDevicePath(deviceCollection);
-      if (deviceCollection.collectionID == -1) {
-        _logger.finest('DeviceCollection should not be -1 here');
+      final collectionID = await _getCollectionID(deviceCollection);
+      if (collectionID == null) {
+        _logger.warning('DeviceCollection was either deleted or missing');
         continue;
       }
 
       moreFilesMarkedForBackup = true;
       await _db.setCollectionIDForUnMappedLocalFiles(
-        deviceCollection.collectionID!,
+        collectionID,
         localIDsToSync,
       );
 
@@ -295,15 +315,13 @@ class RemoteSyncService {
       // the collection. This can happen when a user has marked a folder
       // for sync, then un-synced it and again tries to mark if for sync.
       final Set<String> existingMapping =
-          await _db.getLocalFileIDsForCollection(
-        deviceCollection.collectionID!,
-      );
+          await _db.getLocalFileIDsForCollection(collectionID);
       final Set<String> commonElements =
           localIDsToSync.intersection(existingMapping);
       if (commonElements.isNotEmpty) {
         debugPrint(
           "${commonElements.length} files already existing in "
-          "collection ${deviceCollection.collectionID} for ${deviceCollection.name}",
+          "collection $collectionID for ${deviceCollection.name}",
         );
         localIDsToSync.removeAll(commonElements);
       }
@@ -324,7 +342,7 @@ class RemoteSyncService {
           final String localID = existingFile.localID!;
           if (!fileFoundForLocalIDs.contains(localID)) {
             existingFile.generatedID = null;
-            existingFile.collectionID = deviceCollection.collectionID;
+            existingFile.collectionID = collectionID;
             existingFile.uploadedFileID = null;
             existingFile.ownerID = null;
             newFilesToInsert.add(existingFile);
@@ -414,26 +432,23 @@ class RemoteSyncService {
     }
   }
 
-  Future<void> _createCollectionForDevicePath(
-    DeviceCollection deviceCollection,
-  ) async {
-    int deviceCollectionID = deviceCollection.collectionID ?? -1;
-    if (deviceCollectionID != -1) {
-      final collectionByID =
-          _collectionsService.getCollectionByID(deviceCollectionID);
-      if (collectionByID == null || collectionByID.isDeleted) {
-        _logger.info(
-          "Collection $deviceCollectionID either deleted or missing "
+  Future<int?> _getCollectionID(DeviceCollection deviceCollection) async {
+    if (deviceCollection.collectionID != null) {
+      final collection =
+          _collectionsService.getCollectionByID(deviceCollection.collectionID!);
+      if (collection == null || collection.isDeleted) {
+        _logger.warning(
+          "Collection $deviceCollection.collectionID either deleted or missing "
           "for path ${deviceCollection.id}",
         );
-        deviceCollectionID = -1;
+        return null;
       }
-    }
-    if (deviceCollectionID == -1) {
+      return collection.id;
+    } else {
       final collection =
           await _collectionsService.getOrCreateForPath(deviceCollection.name);
       await _db.updateDeviceCollection(deviceCollection.id, collection.id);
-      deviceCollection.collectionID = collection.id;
+      return collection.id;
     }
   }
 

+ 166 - 1
lib/services/user_service.dart

@@ -1,10 +1,12 @@
 import 'dart:async';
 import 'dart:typed_data';
 
+import 'package:bip39/bip39.dart' as bip39;
 import 'package:dio/dio.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/network/network.dart';
 import 'package:photos/db/public_keys_db.dart';
@@ -23,7 +25,9 @@ import 'package:photos/ui/account/ott_verification_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/password_reentry_page.dart';
 import 'package:photos/ui/account/two_factor_authentication_page.dart';
+import 'package:photos/ui/account/two_factor_recovery_page.dart';
 import 'package:photos/ui/account/two_factor_setup_page.dart';
+import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/toast_util.dart';
@@ -112,6 +116,13 @@ class UserService {
     }
   }
 
+  Future<void> sendFeedback(BuildContext context, String feedback) async {
+    await _dio.post(
+      _config.getHttpEndpoint() + "/anonymous/feedback",
+      data: {"feedback": feedback},
+    );
+  }
+
   // getPublicKey returns null value if email id is not
   // associated with another ente account
   Future<String?> getPublicKey(String email) async {
@@ -489,6 +500,147 @@ class UserService {
     }
   }
 
+  Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    try {
+      final response = await _dio.get(
+        _config.getHttpEndpoint() + "/users/two-factor/recover",
+        queryParameters: {
+          "sessionID": sessionID,
+        },
+      );
+      if (response.statusCode == 200) {
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return TwoFactorRecoveryPage(
+                sessionID,
+                response.data["encryptedSecret"],
+                response.data["secretDecryptionNonce"],
+              );
+            },
+          ),
+          (route) => route.isFirst,
+        );
+      }
+    } on DioError catch (e) {
+      _logger.severe(e);
+      if (e.response != null && e.response!.statusCode == 404) {
+        showToast(context, "Session expired");
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const LoginPage();
+            },
+          ),
+          (route) => route.isFirst,
+        );
+      } else {
+        showErrorDialog(
+          context,
+          "Oops",
+          "Something went wrong, please try again",
+        );
+      }
+    } catch (e) {
+      _logger.severe(e);
+      showErrorDialog(
+        context,
+        "Oops",
+        "Something went wrong, please try again",
+      );
+    } finally {
+      await dialog.hide();
+    }
+  }
+
+  Future<void> removeTwoFactor(
+    BuildContext context,
+    String sessionID,
+    String recoveryKey,
+    String encryptedSecret,
+    String secretDecryptionNonce,
+  ) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    String secret;
+    try {
+      if (recoveryKey.contains(' ')) {
+        if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
+          throw AssertionError(
+            'recovery code should have $mnemonicKeyWordCount words',
+          );
+        }
+        recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
+      }
+      secret = CryptoUtil.bin2base64(
+        await CryptoUtil.decrypt(
+          CryptoUtil.base642bin(encryptedSecret),
+          CryptoUtil.hex2bin(recoveryKey.trim()),
+          CryptoUtil.base642bin(secretDecryptionNonce),
+        ),
+      );
+    } catch (e) {
+      await dialog.hide();
+      await showErrorDialog(
+        context,
+        "Incorrect recovery key",
+        "The recovery key you entered is incorrect",
+      );
+      return;
+    }
+    try {
+      final response = await _dio.post(
+        _config.getHttpEndpoint() + "/users/two-factor/remove",
+        data: {
+          "sessionID": sessionID,
+          "secret": secret,
+        },
+      );
+      if (response.statusCode == 200) {
+        showShortToast(context, "Two-factor authentication successfully reset");
+        await _saveConfiguration(response);
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const PasswordReentryPage();
+            },
+          ),
+          (route) => route.isFirst,
+        );
+      }
+    } on DioError catch (e) {
+      _logger.severe(e);
+      if (e.response != null && e.response!.statusCode == 404) {
+        showToast(context, "Session expired");
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const LoginPage();
+            },
+          ),
+          (route) => route.isFirst,
+        );
+      } else {
+        showErrorDialog(
+          context,
+          "Oops",
+          "Something went wrong, please try again",
+        );
+      }
+    } catch (e) {
+      _logger.severe(e);
+      showErrorDialog(
+        context,
+        "Oops",
+        "Something went wrong, please try again",
+      );
+    } finally {
+      await dialog.hide();
+    }
+  }
+
   Future<void> setupTwoFactor(BuildContext context, Completer completer) async {
     final dialog = createProgressDialog(context, "Please wait...");
     await dialog.show();
@@ -518,13 +670,26 @@ class UserService {
     String secret,
     String code,
   ) async {
+    Uint8List recoveryKey;
+    try {
+      recoveryKey = await getOrCreateRecoveryKey(context);
+    } catch (e) {
+      showGenericErrorDialog(context: context);
+      return false;
+    }
     final dialog = createProgressDialog(context, "Verifying...");
     await dialog.show();
+    final encryptionResult =
+        CryptoUtil.encryptSync(CryptoUtil.base642bin(secret), recoveryKey);
     try {
       await _enteDio.post(
         "/users/two-factor/enable",
         data: {
-          "code": code
+          "code": code,
+          "encryptedTwoFactorSecret":
+              CryptoUtil.bin2base64(encryptionResult.encryptedData!),
+          "twoFactorSecretDecryptionNonce":
+              CryptoUtil.bin2base64(encryptionResult.nonce!),
         },
       );
       await dialog.hide();

+ 22 - 1
lib/ui/account/delete_account_page.dart

@@ -1,12 +1,13 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
+import "package:logging/logging.dart";
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/delete_account.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -157,7 +158,27 @@ class DeleteAccountPage extends StatelessWidget {
       if (choice.action != ButtonAction.first) {
         return;
       }
+
       Navigator.of(context).popUntil((route) => route.isFirst);
+      await showTextInputDialog(
+        context,
+        title: "Your account was deleted. Would you like to leave us a note?",
+        submitButtonLabel: "Send",
+        hintText: "Optional, as short as you like...",
+        alwaysShowSuccessState: true,
+        textCapitalization: TextCapitalization.words,
+        onSubmit: (String text) async {
+          // indicates user cancelled the rename request
+          if (text == "" || text.trim().isEmpty) {
+            return;
+          }
+          try {
+            await UserService.instance.sendFeedback(context, text);
+          } catch (e, s) {
+            Logger("Delete account").severe("Failed to send feedback", e, s);
+          }
+        },
+      );
     }
   }
 

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

@@ -8,7 +8,7 @@ import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/ui/account/recovery_page.dart';
 import 'package:photos/ui/common/dynamic_fab.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/home_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/email_util.dart';

+ 1 - 6
lib/ui/account/two_factor_authentication_page.dart

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/lifecycle_event_handler.dart';
-import "package:photos/utils/dialog_util.dart";
 import 'package:pinput/pin_put/pin_put.dart';
 
 class TwoFactorAuthenticationPage extends StatefulWidget {
@@ -124,11 +123,7 @@ class _TwoFactorAuthenticationPageState
         GestureDetector(
           behavior: HitTestBehavior.opaque,
           onTap: () {
-            showErrorDialog(
-              context,
-              "Contact support",
-              "Please drop an email to support@ente.io from your registered email address",
-            );
+            UserService.instance.recoverTwoFactor(context, widget.sessionID);
           },
           child: Container(
             padding: const EdgeInsets.all(10),

+ 110 - 0
lib/ui/account/two_factor_recovery_page.dart

@@ -0,0 +1,110 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:photos/services/user_service.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+class TwoFactorRecoveryPage extends StatefulWidget {
+  final String sessionID;
+  final String encryptedSecret;
+  final String secretDecryptionNonce;
+
+  const TwoFactorRecoveryPage(
+    this.sessionID,
+    this.encryptedSecret,
+    this.secretDecryptionNonce, {
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<TwoFactorRecoveryPage> createState() => _TwoFactorRecoveryPageState();
+}
+
+class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
+  final _recoveryKey = TextEditingController();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text(
+          "Recover account",
+          style: TextStyle(
+            fontSize: 18,
+          ),
+        ),
+      ),
+      body: Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        mainAxisAlignment: MainAxisAlignment.center,
+        mainAxisSize: MainAxisSize.max,
+        children: [
+          Padding(
+            padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
+            child: TextFormField(
+              decoration: const InputDecoration(
+                hintText: "Enter your recovery key",
+                contentPadding: EdgeInsets.all(20),
+              ),
+              style: const TextStyle(
+                fontSize: 14,
+                fontFeatures: [FontFeature.tabularFigures()],
+              ),
+              controller: _recoveryKey,
+              autofocus: false,
+              autocorrect: false,
+              keyboardType: TextInputType.multiline,
+              maxLines: null,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+          ),
+          const Padding(padding: EdgeInsets.all(24)),
+          Container(
+            padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
+            width: double.infinity,
+            height: 64,
+            child: OutlinedButton(
+              onPressed: _recoveryKey.text.isNotEmpty
+                  ? () async {
+                      await UserService.instance.removeTwoFactor(
+                        context,
+                        widget.sessionID,
+                        _recoveryKey.text,
+                        widget.encryptedSecret,
+                        widget.secretDecryptionNonce,
+                      );
+                    }
+                  : null,
+              child: const Text("Recover"),
+            ),
+          ),
+          GestureDetector(
+            behavior: HitTestBehavior.translucent,
+            onTap: () {
+              showErrorDialog(
+                context,
+                "Contact support",
+                "Please drop an email to support@ente.io from your registered email address",
+              );
+            },
+            child: Container(
+              padding: const EdgeInsets.all(40),
+              child: Center(
+                child: Text(
+                  "No recovery key?",
+                  style: TextStyle(
+                    decoration: TextDecoration.underline,
+                    fontSize: 12,
+                    color: Colors.white.withOpacity(0.9),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

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

@@ -12,7 +12,7 @@ import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/account/recovery_key_page.dart';
 import 'package:photos/ui/common/gradient_button.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/navigation_util.dart';

+ 1 - 1
lib/ui/actions/collection/collection_file_actions.dart

@@ -6,7 +6,7 @@ import 'package:photos/services/favorites_service.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';

+ 1 - 1
lib/ui/actions/collection/collection_sharing_actions.dart

@@ -15,7 +15,7 @@ import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/payment/subscription.dart';

+ 3 - 3
lib/ui/actions/file/file_actions.dart

@@ -5,9 +5,9 @@ import "package:photos/models/file_type.dart";
 import "package:photos/theme/colors.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/components/action_sheet_widget.dart";
-import "package:photos/ui/components/button_widget.dart";
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import "package:photos/ui/components/models/button_type.dart";
-import "package:photos/ui/viewer/file/file_info_widget.dart";
+import 'package:photos/ui/viewer/file/file_details_widget.dart';
 import "package:photos/utils/delete_file_util.dart";
 import "package:photos/utils/dialog_util.dart";
 import "package:photos/utils/toast_util.dart";
@@ -136,7 +136,7 @@ Future<void> showInfoSheet(BuildContext context, File file) async {
       return Padding(
         padding:
             EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
-        child: FileInfoWidget(file),
+        child: FileDetailsWidget(file),
       );
     },
   );

+ 1 - 1
lib/ui/advanced_settings_screen.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/components/title_bar_widget.dart';

+ 1 - 1
lib/ui/backup_settings_screen.dart

@@ -3,9 +3,9 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
 import 'package:photos/ui/components/menu_section_description_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';

+ 35 - 36
lib/ui/collection_action_sheet.dart

@@ -14,7 +14,7 @@ import 'package:photos/theme/ente_theme.dart';
 import "package:photos/ui/collections_list_widget.dart";
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/components/bottom_of_title_bar_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import "package:photos/ui/components/text_input_widget.dart";
 import 'package:photos/ui/components/title_bar_title_widget.dart';
@@ -153,7 +153,6 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
                           child: TextInputWidget(
                             hintText: "Album name",
                             prefixIcon: Icons.search_rounded,
-                            autoFocus: true,
                             onChange: (value) {
                               setState(() {
                                 _searchQuery = value;
@@ -199,33 +198,33 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
     return Flexible(
       child: Padding(
         padding: const EdgeInsets.fromLTRB(16, 24, 4, 0),
-        child: Scrollbar(
-          thumbVisibility: true,
-          radius: const Radius.circular(2),
-          child: Padding(
-            padding: const EdgeInsets.only(right: 12),
-            child: FutureBuilder(
-              future: _getCollectionsWithThumbnail(),
-              builder: (context, snapshot) {
-                if (snapshot.hasError) {
-                  //Need to show an error on the UI here
-                  return const SizedBox.shrink();
-                } else if (snapshot.hasData) {
-                  final collectionsWithThumbnail =
-                      snapshot.data as List<CollectionWithThumbnail>;
-                  _removeIncomingCollections(collectionsWithThumbnail);
-                  final shouldShowCreateAlbum =
-                      widget.showOptionToCreateNewAlbum && _searchQuery.isEmpty;
-                  final searchResults = _searchQuery.isNotEmpty
-                      ? collectionsWithThumbnail
-                          .where(
-                            (element) => element.collection.name!
-                                .toLowerCase()
-                                .contains(_searchQuery),
-                          )
-                          .toList()
-                      : collectionsWithThumbnail;
-                  return CollectionsListWidget(
+        child: FutureBuilder(
+          future: _getCollectionsWithThumbnail(),
+          builder: (context, snapshot) {
+            if (snapshot.hasError) {
+              //Need to show an error on the UI here
+              return const SizedBox.shrink();
+            } else if (snapshot.hasData) {
+              final collectionsWithThumbnail =
+                  snapshot.data as List<CollectionWithThumbnail>;
+              _removeIncomingCollections(collectionsWithThumbnail);
+              final shouldShowCreateAlbum =
+                  widget.showOptionToCreateNewAlbum && _searchQuery.isEmpty;
+              final searchResults = _searchQuery.isNotEmpty
+                  ? collectionsWithThumbnail
+                      .where(
+                        (element) => element.collection.name!
+                            .toLowerCase()
+                            .contains(_searchQuery),
+                      )
+                      .toList()
+                  : collectionsWithThumbnail;
+              return Scrollbar(
+                thumbVisibility: true,
+                radius: const Radius.circular(2),
+                child: Padding(
+                  padding: const EdgeInsets.only(right: 12),
+                  child: CollectionsListWidget(
                     searchResults,
                     widget.actionType,
                     widget.showOptionToCreateNewAlbum,
@@ -233,13 +232,13 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
                     widget.sharedFiles,
                     _searchQuery,
                     shouldShowCreateAlbum,
-                  );
-                } else {
-                  return const EnteLoadingWidget();
-                }
-              },
-            ),
-          ),
+                  ),
+                ),
+              );
+            } else {
+              return const EnteLoadingWidget();
+            }
+          },
         ),
       ),
     );

+ 1 - 1
lib/ui/collections_gallery_widget.dart

@@ -21,7 +21,7 @@ import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/trash_button_widget.dart';
 import 'package:photos/ui/collections/uncat_collections_button_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
-import "package:photos/ui/components/icon_button_widget.dart";
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/viewer/actions/delete_empty_albums.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/utils/local_settings.dart';

+ 14 - 6
lib/ui/common/loading_widget.dart

@@ -3,17 +3,25 @@ import 'package:photos/theme/ente_theme.dart';
 
 class EnteLoadingWidget extends StatelessWidget {
   final Color? color;
-  final bool is20pts;
-  const EnteLoadingWidget({this.is20pts = false, this.color, Key? key})
-      : super(key: key);
+  final double size;
+  final double padding;
+  final Alignment alignment;
+  const EnteLoadingWidget({
+    this.color,
+    this.size = 14,
+    this.padding = 5,
+    this.alignment = Alignment.center,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return Center(
+    return Align(
+      alignment: alignment,
       child: Padding(
-        padding: EdgeInsets.all(is20pts ? 3 : 5),
+        padding: EdgeInsets.all(padding),
         child: SizedBox.fromSize(
-          size: const Size.square(14),
+          size: Size.square(size),
           child: CircularProgressIndicator(
             strokeWidth: 2,
             color: color ?? getEnteColorScheme(context).strokeBase,

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

@@ -7,7 +7,7 @@ import "package:photos/models/search/button_result.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/ui/components/buttons/button_widget.dart';
 import 'package:photos/utils/separators_util.dart';
 
 enum ActionSheetType {

+ 1 - 1
lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart

@@ -8,7 +8,7 @@ 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';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 
 class BottomActionBarWidget extends StatelessWidget {
   final String? text;

+ 1 - 1
lib/ui/components/button_widget.dart → lib/ui/components/buttons/button_widget.dart

@@ -350,7 +350,7 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
                                       },
                                     ),
                               EnteLoadingWidget(
-                                is20pts: true,
+                                padding: 3,
                                 color: loadingIconColor,
                               ),
                             ],

+ 54 - 0
lib/ui/components/buttons/chip_button_widget.dart

@@ -0,0 +1,54 @@
+import "package:flutter/material.dart";
+import "package:photos/theme/ente_theme.dart";
+
+///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=8119%3A59513&t=gQa1to5jY89Qk1k7-4
+class ChipButtonWidget extends StatelessWidget {
+  final String? label;
+  final IconData? leadingIcon;
+  final VoidCallback? onTap;
+  final bool noChips;
+  const ChipButtonWidget(
+    this.label, {
+    this.leadingIcon,
+    this.onTap,
+    this.noChips = false,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: onTap?.call,
+      child: Container(
+        width: noChips ? double.infinity : null,
+        decoration: BoxDecoration(
+          color: getEnteColorScheme(context).fillFaint,
+          borderRadius: const BorderRadius.all(Radius.circular(4)),
+        ),
+        child: Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: Row(
+            mainAxisSize: MainAxisSize.min,
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              leadingIcon != null
+                  ? Icon(
+                      leadingIcon,
+                      size: 17,
+                    )
+                  : const SizedBox.shrink(),
+              const SizedBox(width: 4),
+              Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 4),
+                child: Text(
+                  label ?? "",
+                  style: getEnteTextTheme(context).smallBold,
+                ),
+              )
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 4 - 3
lib/ui/components/icon_button_widget.dart → lib/ui/components/buttons/icon_button_widget.dart

@@ -43,6 +43,7 @@ class _IconButtonWidgetState extends State<IconButtonWidget> {
 
   @override
   Widget build(BuildContext context) {
+    final bool hasPressedState = widget.onTap != null;
     final colorTheme = getEnteColorScheme(context);
     iconStateColor ??
         (iconStateColor = widget.defaultColor ??
@@ -52,9 +53,9 @@ class _IconButtonWidgetState extends State<IconButtonWidget> {
     return widget.disableGestureDetector
         ? _iconButton(colorTheme)
         : GestureDetector(
-            onTapDown: _onTapDown,
-            onTapUp: _onTapUp,
-            onTapCancel: _onTapCancel,
+            onTapDown: hasPressedState ? _onTapDown : null,
+            onTapUp: hasPressedState ? _onTapUp : null,
+            onTapCancel: hasPressedState ? _onTapCancel : null,
             onTap: widget.onTap,
             child: _iconButton(colorTheme),
           );

+ 20 - 0
lib/ui/components/buttons/inline_button_widget.dart

@@ -0,0 +1,20 @@
+import "package:flutter/cupertino.dart";
+import "package:photos/theme/ente_theme.dart";
+
+class InlineButtonWidget extends StatelessWidget {
+  final String label;
+  final VoidCallback? onTap;
+  final TextStyle? textStyle;
+  const InlineButtonWidget(this.label, this.onTap, {this.textStyle, super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: onTap?.call,
+      child: Text(
+        label,
+        style: textStyle ?? getEnteTextTheme(context).smallMuted,
+      ),
+    );
+  }
+}

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

@@ -7,7 +7,7 @@ import 'package:photos/models/typedefs.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/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/components/text_input_widget.dart';
 import 'package:photos/utils/separators_util.dart';

+ 8 - 1
lib/ui/components/divider_widget.dart

@@ -11,15 +11,21 @@ enum DividerType {
 class DividerWidget extends StatelessWidget {
   final DividerType dividerType;
   final Color bgColor;
+  final bool divColorHasBlur;
+  final EdgeInsets? padding;
   const DividerWidget({
     required this.dividerType,
     this.bgColor = Colors.transparent,
+    this.divColorHasBlur = true,
+    this.padding,
     super.key,
   });
 
   @override
   Widget build(BuildContext context) {
-    final dividerColor = getEnteColorScheme(context).blurStrokeFaint;
+    final dividerColor = divColorHasBlur
+        ? getEnteColorScheme(context).blurStrokeFaint
+        : getEnteColorScheme(context).strokeFaint;
 
     if (dividerType == DividerType.solid) {
       return Container(
@@ -38,6 +44,7 @@ class DividerWidget extends StatelessWidget {
 
     return Container(
       color: bgColor,
+      padding: padding ?? EdgeInsets.zero,
       child: Row(
         children: [
           SizedBox(

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

@@ -1,5 +1,5 @@
 import 'package:flutter/material.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/viewer/search/search_widget.dart';
 
 class HomeHeaderWidget extends StatefulWidget {

+ 99 - 0
lib/ui/components/info_item_widget.dart

@@ -0,0 +1,99 @@
+import "package:flutter/material.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/common/loading_widget.dart";
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
+
+///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=8113-59605&t=OMX5f5KdDJYWSQQN-4
+class InfoItemWidget extends StatelessWidget {
+  final IconData leadingIcon;
+  final VoidCallback? editOnTap;
+  final String title;
+  final Future<List<Widget>> subtitleSection;
+  final bool hasChipButtons;
+  const InfoItemWidget({
+    required this.leadingIcon,
+    this.editOnTap,
+    required this.title,
+    required this.subtitleSection,
+    this.hasChipButtons = false,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Flexible(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              IconButtonWidget(
+                icon: leadingIcon,
+                iconButtonType: IconButtonType.secondary,
+              ),
+              Flexible(
+                child: Padding(
+                  padding: const EdgeInsets.fromLTRB(12, 3.5, 16, 3.5),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        title,
+                        style: hasChipButtons
+                            ? getEnteTextTheme(context).smallMuted
+                            : getEnteTextTheme(context).body,
+                      ),
+                      SizedBox(height: hasChipButtons ? 8 : 4),
+                      Flexible(
+                        child: FutureBuilder(
+                          future: subtitleSection,
+                          builder: (context, snapshot) {
+                            Widget child;
+                            if (snapshot.hasData) {
+                              final subtitle = snapshot.data as List<Widget>;
+                              if (subtitle.isNotEmpty) {
+                                child = Wrap(
+                                  runSpacing: 8,
+                                  spacing: 8,
+                                  children: subtitle,
+                                );
+                              } else {
+                                child = const SizedBox.shrink();
+                              }
+                            } else {
+                              child = EnteLoadingWidget(
+                                padding: 3,
+                                size: 11,
+                                color: getEnteColorScheme(context).strokeMuted,
+                                alignment: Alignment.centerLeft,
+                              );
+                            }
+                            return AnimatedSwitcher(
+                              duration: const Duration(milliseconds: 200),
+                              switchInCurve: Curves.easeInOutExpo,
+                              child: child,
+                            );
+                          },
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+        editOnTap != null
+            ? IconButtonWidget(
+                icon: Icons.edit,
+                iconButtonType: IconButtonType.secondary,
+                onTap: editOnTap,
+              )
+            : const SizedBox.shrink(),
+      ],
+    );
+  }
+}

+ 1 - 1
lib/ui/components/models/button_type.dart

@@ -1,7 +1,7 @@
 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';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 
 enum ButtonType {
   primary,

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

@@ -3,7 +3,7 @@ import 'package:photos/ente_theme_data.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/components/icon_button_widget.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 
 // CreateNotificationType enum
 enum NotificationType {

+ 5 - 3
lib/ui/components/text_input_widget.dart

@@ -77,9 +77,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
         selection: TextSelection.collapsed(offset: widget.initialValue!.length),
       );
     }
-    _textController.addListener(() {
-      widget.onChange!.call(_textController.text);
-    });
+    if (widget.onChange != null) {
+      _textController.addListener(() {
+        widget.onChange!.call(_textController.text);
+      });
+    }
     _obscureTextNotifier = ValueNotifier(widget.isPasswordInput);
     _obscureTextNotifier.addListener(_safeRefresh);
     super.initState();

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

@@ -1,6 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 
 class TitleBarWidget extends StatelessWidget {
   final IconButtonWidget? leading;

+ 2 - 2
lib/ui/growth/apply_code_screen.dart

@@ -5,8 +5,8 @@ import "package:photos/models/api/storage_bonus/storage_bonus.dart";
 import "package:photos/models/user_details.dart";
 import "package:photos/services/storage_bonus_service.dart";
 import "package:photos/theme/ente_theme.dart";
-import "package:photos/ui/components/button_widget.dart";
-import "package:photos/ui/components/icon_button_widget.dart";
+import 'package:photos/ui/components/buttons/button_widget.dart';
+import 'package:photos/ui/components/buttons/icon_button_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_widget.dart";

+ 1 - 1
lib/ui/growth/code_success_screen.dart

@@ -3,8 +3,8 @@ import "package:flutter_animate/flutter_animate.dart";
 import "package:photos/models/api/storage_bonus/storage_bonus.dart";
 import "package:photos/models/user_details.dart";
 import "package:photos/theme/ente_theme.dart";
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import "package:photos/ui/components/captioned_text_widget.dart";
-import "package:photos/ui/components/icon_button_widget.dart";
 import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
 import "package:photos/ui/components/title_bar_title_widget.dart";
 import "package:photos/ui/components/title_bar_widget.dart";

+ 1 - 1
lib/ui/growth/referral_screen.dart

@@ -6,9 +6,9 @@ import "package:photos/services/user_service.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/common/loading_widget.dart";
 import "package:photos/ui/common/web_page.dart";
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import "package:photos/ui/components/captioned_text_widget.dart";
 import "package:photos/ui/components/divider_widget.dart";
-import "package:photos/ui/components/icon_button_widget.dart";
 import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
 import "package:photos/ui/components/title_bar_title_widget.dart";
 import "package:photos/ui/components/title_bar_widget.dart";

+ 1 - 1
lib/ui/growth/storage_details_screen.dart

@@ -6,7 +6,7 @@ import "package:photos/models/user_details.dart";
 import "package:photos/services/storage_bonus_service.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/common/loading_widget.dart";
-import "package:photos/ui/components/icon_button_widget.dart";
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import "package:photos/ui/components/title_bar_title_widget.dart";
 import "package:photos/ui/components/title_bar_widget.dart";
 import "package:photos/utils/data_util.dart";

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

@@ -10,7 +10,7 @@ import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/password_reentry_page.dart';
 import 'package:photos/ui/common/gradient_button.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/payment/subscription.dart';

+ 9 - 13
lib/ui/home/memories_widget.dart

@@ -2,9 +2,9 @@ import "dart:io";
 
 import "package:flutter/cupertino.dart";
 import 'package:flutter/material.dart';
-import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/memory.dart';
 import 'package:photos/services/memories_service.dart';
+import "package:photos/theme/ente_theme.dart";
 import "package:photos/theme/text_style.dart";
 import "package:photos/ui/actions/file/file_actions.dart";
 import "package:photos/ui/extents_page_view.dart";
@@ -120,10 +120,7 @@ class _MemoryWidgetState extends State<MemoryWidget> {
                   type: MaterialType.transparency,
                   child: Text(
                     title,
-                    style: Theme.of(context)
-                        .textTheme
-                        .subtitle1!
-                        .copyWith(fontSize: 12),
+                    style: getEnteTextTheme(context).mini,
                     textAlign: TextAlign.center,
                   ),
                 ),
@@ -136,22 +133,21 @@ class _MemoryWidgetState extends State<MemoryWidget> {
   }
 
   Container _buildMemoryItem(BuildContext context, int index) {
+    final colorScheme = getEnteColorScheme(context);
     final memory = widget.memories[index];
     final isSeen = memory.isSeen();
     return Container(
       decoration: BoxDecoration(
-        border: isSeen
-            ? const Border()
-            : Border.all(
-                color: Theme.of(context).colorScheme.greenAlternative,
-                width: isSeen ? 0 : 2,
-              ),
+        border: Border.all(
+          color: isSeen ? colorScheme.strokeFaint : colorScheme.primary500,
+          width: 2,
+        ),
         borderRadius: BorderRadius.circular(40),
       ),
       child: ClipOval(
         child: SizedBox(
-          width: isSeen ? 60 : 56,
-          height: isSeen ? 60 : 56,
+          width: 56,
+          height: 56,
           child: Hero(
             tag: "memories" + memory.file.tag,
             child: ThumbnailWidget(

+ 4 - 0
lib/ui/home/status_bar_widget.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 
 import 'package:flutter/material.dart';
+import "package:logging/logging.dart";
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/notification_event.dart';
@@ -24,6 +25,8 @@ class StatusBarWidget extends StatefulWidget {
 }
 
 class _StatusBarWidgetState extends State<StatusBarWidget> {
+  static final _logger = Logger("StatusBarWidget");
+
   late StreamSubscription<SyncStatusUpdate> _subscription;
   late StreamSubscription<NotificationEvent> _notificationSubscription;
   bool _showStatus = false;
@@ -33,6 +36,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
   @override
   void initState() {
     _subscription = Bus.instance.on<SyncStatusUpdate>().listen((event) {
+      _logger.info("Received event " + event.toString());
       if (event.status == SyncStatus.error) {
         setState(() {
           _syncError = event.error;

+ 1 - 1
lib/ui/new_shared_collections_gallery.dart

@@ -2,7 +2,7 @@ import "package:flutter/material.dart";
 import "package:photos/core/constants.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/collection_action_sheet.dart";
-import "package:photos/ui/components/button_widget.dart";
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import "package:photos/ui/components/empty_state_item_widget.dart";
 import "package:photos/ui/components/models/button_type.dart";
 import "package:photos/utils/share_util.dart";

+ 1 - 1
lib/ui/notification/update/change_log_page.dart

@@ -3,7 +3,7 @@ import "dart:io";
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_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';

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

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/services/user_service.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/utils/dialog_util.dart';
 
 class ChildSubscriptionWidget extends StatelessWidget {

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

@@ -14,7 +14,7 @@ import 'package:photos/ui/common/bottom_shadow.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/ui/common/web_page.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import "package:photos/ui/components/captioned_text_widget.dart";
 import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
 import 'package:photos/ui/payment/child_subscription_widget.dart';

+ 1 - 1
lib/ui/sharing/add_partipant_page.dart

@@ -5,7 +5,7 @@ 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/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.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/menu_item_widget.dart';

+ 1 - 1
lib/ui/sharing/manage_album_participant.dart

@@ -4,7 +4,7 @@ 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/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.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/menu_item_widget.dart';

+ 1 - 1
lib/ui/sharing/verify_identity_dialog.dart

@@ -10,7 +10,7 @@ import "package:photos/core/configuration.dart";
 import "package:photos/services/user_service.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/common/loading_widget.dart";
-import "package:photos/ui/components/button_widget.dart";
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import "package:photos/ui/components/models/button_type.dart";
 import "package:photos/utils/share_util.dart";
 

+ 3 - 1
lib/ui/tools/app_lock.dart

@@ -32,11 +32,13 @@ class AppLock extends StatefulWidget {
   final Duration backgroundLockLatency;
   final ThemeData? darkTheme;
   final ThemeData? lightTheme;
+  final ThemeMode savedThemeMode;
 
   const AppLock({
     Key? key,
     required this.builder,
     required this.lockScreen,
+    required this.savedThemeMode,
     this.enabled = true,
     this.backgroundLockLatency = const Duration(seconds: 0),
     this.darkTheme,
@@ -103,7 +105,7 @@ class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
     return MaterialApp(
       home: this.widget.enabled ? this._lockScreen : this.widget.builder(null),
       navigatorKey: _navigatorKey,
-      themeMode: ThemeMode.system,
+      themeMode: widget.savedThemeMode,
       theme: widget.lightTheme,
       darkTheme: widget.darkTheme,
       supportedLocales: AppLocalizations.supportedLocales,

+ 1 - 1
lib/ui/tools/debug/app_storage_viewer.dart

@@ -7,8 +7,8 @@ import 'package:photos/core/cache/video_cache_manager.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
 import 'package:photos/ui/components/menu_section_title.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';

+ 1 - 1
lib/ui/tools/editor/image_editor_page.dart

@@ -16,7 +16,7 @@ import 'package:photos/models/location.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/tools/editor/filtered_image.dart';
 import 'package:photos/ui/viewer/file/detail_page.dart';

+ 1 - 1
lib/ui/viewer/actions/delete_empty_albums.dart

@@ -5,7 +5,7 @@ import 'package:photos/models/collection.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 
 class DeleteEmptyAlbums extends StatefulWidget {

+ 1 - 1
lib/ui/viewer/actions/file_selection_actions_widget.dart

@@ -18,7 +18,7 @@ import 'package:photos/ui/collection_action_sheet.dart';
 import 'package:photos/ui/components/action_sheet_widget.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/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/sharing/manage_links_widget.dart';
 import 'package:photos/utils/delete_file_util.dart';

+ 1 - 1
lib/ui/viewer/actions/file_selection_overlay_bar.dart

@@ -7,7 +7,7 @@ import 'package:photos/models/selected_files.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/collection_action_sheet.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/components/buttons/icon_button_widget.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';

+ 0 - 68
lib/ui/viewer/file/collections_list_of_file_widget.dart

@@ -1,68 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
-import 'package:photos/models/collection.dart';
-import 'package:photos/models/collection_items.dart';
-import 'package:photos/models/gallery_type.dart';
-import 'package:photos/services/collections_service.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/gallery/collection_page.dart';
-import 'package:photos/utils/navigation_util.dart';
-
-class CollectionsListOfFileWidget extends StatelessWidget {
-  final Future<Set<int>> allCollectionIDsOfFile;
-  final int currentUserID;
-
-  const CollectionsListOfFileWidget(
-    this.allCollectionIDsOfFile,
-    this.currentUserID, {
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FutureBuilder<Set<int>>(
-      future: allCollectionIDsOfFile,
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final Set<int> collectionIDs = snapshot.data!;
-          final collections = <Collection>[];
-          for (var collectionID in collectionIDs) {
-            final c =
-                CollectionsService.instance.getCollectionByID(collectionID);
-            collections.add(c!);
-          }
-          return ListView.builder(
-            itemCount: collections.length,
-            scrollDirection: Axis.horizontal,
-            itemBuilder: (context, index) {
-              final bool isHidden = collections[index].isHidden();
-              return FileInfoCollectionWidget(
-                name: isHidden ? 'Hidden' : collections[index].name,
-                onTap: () {
-                  if (isHidden) {
-                    return;
-                  }
-                  routeToPage(
-                    context,
-                    CollectionPage(
-                      CollectionWithThumbnail(collections[index], null),
-                      appBarType: collections[index].isOwner(currentUserID)
-                          ? GalleryType.ownedCollection
-                          : GalleryType.sharedCollection,
-                    ),
-                  );
-                },
-              );
-            },
-          );
-        } else if (snapshot.hasError) {
-          Logger("CollectionsListOfFile").info(snapshot.error);
-          return const SizedBox.shrink();
-        } else {
-          return const EnteLoadingWidget();
-        }
-      },
-    );
-  }
-}

+ 0 - 37
lib/ui/viewer/file/device_folders_list_of_file_widget.dart

@@ -1,37 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
-import 'package:photos/ui/common/loading_widget.dart';
-import 'package:photos/ui/viewer/file/file_info_collection_widget.dart';
-
-class DeviceFoldersListOfFileWidget extends StatelessWidget {
-  final Future<Set<String>> allDeviceFoldersOfFile;
-  const DeviceFoldersListOfFileWidget(this.allDeviceFoldersOfFile, {Key? key})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FutureBuilder<Set<String>>(
-      future: allDeviceFoldersOfFile,
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final List<String> deviceFolders = snapshot.data!.toList();
-          return ListView.builder(
-            itemCount: deviceFolders.length,
-            scrollDirection: Axis.horizontal,
-            itemBuilder: (context, index) {
-              return FileInfoCollectionWidget(
-                name: deviceFolders[index],
-                onTap: () {},
-              );
-            },
-          );
-        } else if (snapshot.hasError) {
-          Logger("DeviceFoldersListOfFile").info(snapshot.error);
-          return const SizedBox.shrink();
-        } else {
-          return const EnteLoadingWidget();
-        }
-      },
-    );
-  }
-}

+ 17 - 14
lib/ui/viewer/file/exif_info_dialog.dart

@@ -2,31 +2,34 @@ import 'dart:ui';
 
 import 'package:flutter/material.dart';
 import 'package:photos/models/file.dart';
+import "package:photos/theme/ente_theme.dart";
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/utils/exif_util.dart';
 
-class ExifInfoDialog extends StatefulWidget {
+class ExifInfoDialog extends StatelessWidget {
   final File file;
   const ExifInfoDialog(this.file, {Key? key}) : super(key: key);
 
-  @override
-  State<ExifInfoDialog> createState() => _ExifInfoDialogState();
-}
-
-class _ExifInfoDialogState extends State<ExifInfoDialog> {
   @override
   Widget build(BuildContext context) {
-    final scrollController = ScrollController();
+    final textTheme = getEnteTextTheme(context);
     return AlertDialog(
-      title: Text(
-        widget.file.title!,
-        style: Theme.of(context).textTheme.headline5,
+      title: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            "EXIF",
+            style: textTheme.h3Bold,
+          ),
+          Text(
+            file.title!,
+            style: textTheme.smallMuted,
+          ),
+        ],
       ),
       content: Scrollbar(
-        controller: scrollController,
         thumbVisibility: true,
         child: SingleChildScrollView(
-          controller: scrollController,
           child: _getInfo(),
         ),
       ),
@@ -34,7 +37,7 @@ class _ExifInfoDialogState extends State<ExifInfoDialog> {
         TextButton(
           child: Text(
             "Close",
-            style: Theme.of(context).textTheme.bodyText1,
+            style: textTheme.body,
           ),
           onPressed: () {
             Navigator.of(context, rootNavigator: true).pop('dialog');
@@ -46,7 +49,7 @@ class _ExifInfoDialogState extends State<ExifInfoDialog> {
 
   Widget _getInfo() {
     return FutureBuilder(
-      future: getExif(widget.file),
+      future: getExif(file),
       builder: (BuildContext context, AsyncSnapshot snapshot) {
         if (snapshot.hasData) {
           final exif = snapshot.data;

+ 255 - 0
lib/ui/viewer/file/file_details_widget.dart

@@ -0,0 +1,255 @@
+import "package:exif/exif.dart";
+import "package:flutter/cupertino.dart";
+import "package:flutter/material.dart";
+import "package:photos/core/configuration.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/file_type.dart";
+import "package:photos/services/feature_flag_service.dart";
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
+import "package:photos/ui/components/divider_widget.dart";
+import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/ui/viewer/file/file_caption_widget.dart';
+import "package:photos/ui/viewer/file_details/added_by_widget.dart";
+import "package:photos/ui/viewer/file_details/albums_item_widget.dart";
+import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
+import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
+import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
+import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
+import "package:photos/ui/viewer/file_details/objects_item_widget.dart";
+import "package:photos/utils/exif_util.dart";
+
+class FileDetailsWidget extends StatefulWidget {
+  final File file;
+  const FileDetailsWidget(
+    this.file, {
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<FileDetailsWidget> createState() => _FileDetailsWidgetState();
+}
+
+class _FileDetailsWidgetState extends State<FileDetailsWidget> {
+  final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
+  final Map<String, dynamic> _exifData = {
+    "focalLength": null,
+    "fNumber": null,
+    "resolution": null,
+    "takenOnDevice": null,
+    "exposureTime": null,
+    "ISO": null,
+    "megaPixels": null
+  };
+
+  bool _isImage = false;
+  late int _currentUserID;
+  bool showExifListTile = false;
+
+  @override
+  void initState() {
+    debugPrint('file_details_sheet initState');
+    _currentUserID = Configuration.instance.getUserID()!;
+    _isImage = widget.file.fileType == FileType.image ||
+        widget.file.fileType == FileType.livePhoto;
+    if (_isImage) {
+      _exifNotifier.addListener(() {
+        if (_exifNotifier.value != null) {
+          _generateExifForDetails(_exifNotifier.value!);
+        }
+        showExifListTile = _exifData["focalLength"] != null ||
+            _exifData["fNumber"] != null ||
+            _exifData["takenOnDevice"] != null ||
+            _exifData["exposureTime"] != null ||
+            _exifData["ISO"] != null;
+      });
+      getExif(widget.file).then((exif) {
+        _exifNotifier.value = exif;
+      });
+    }
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _exifNotifier.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final file = widget.file;
+    final bool isFileOwner =
+        file.ownerID == null || file.ownerID == _currentUserID;
+
+    //Make sure the bottom most tile is always the same one, that is it should
+    //not be rendered only if a condition is met.
+    final fileDetailsTiles = <Widget>[];
+    fileDetailsTiles.add(
+      !widget.file.isUploaded ||
+              (!isFileOwner && (widget.file.caption?.isEmpty ?? true))
+          ? const SizedBox(height: 16)
+          : Padding(
+              padding: const EdgeInsets.only(top: 8, bottom: 24),
+              child: isFileOwner
+                  ? FileCaptionWidget(file: widget.file)
+                  : FileCaptionReadyOnly(caption: widget.file.caption!),
+            ),
+    );
+    fileDetailsTiles.addAll([
+      CreationTimeItem(file, _currentUserID),
+      const FileDetailsDivider(),
+      ValueListenableBuilder(
+        valueListenable: _exifNotifier,
+        builder: (context, _, __) => FilePropertiesItemWidget(
+          file,
+          _isImage,
+          _exifData,
+          _currentUserID,
+        ),
+      ),
+      const FileDetailsDivider(),
+    ]);
+    fileDetailsTiles.add(
+      ValueListenableBuilder(
+        valueListenable: _exifNotifier,
+        builder: (context, value, _) {
+          return showExifListTile
+              ? Column(
+                  children: [
+                    BasicExifItemWidget(_exifData),
+                    const FileDetailsDivider(),
+                  ],
+                )
+              : const SizedBox.shrink();
+        },
+      ),
+    );
+    if (_isImage) {
+      fileDetailsTiles.addAll([
+        ValueListenableBuilder(
+          valueListenable: _exifNotifier,
+          builder: (context, value, _) {
+            return Column(
+              children: [
+                AllExifItemWidget(file, _exifNotifier.value),
+                const FileDetailsDivider(),
+              ],
+            );
+          },
+        )
+      ]);
+    }
+    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
+      fileDetailsTiles.addAll([
+        ObjectsItemWidget(file),
+        const FileDetailsDivider(),
+      ]);
+    }
+    if (file.uploadedFileID != null && file.updationTime != null) {
+      fileDetailsTiles.addAll(
+        [
+          BackedUpTimeItemWidget(file),
+          const FileDetailsDivider(),
+        ],
+      );
+    }
+    fileDetailsTiles.add(AlbumsItemWidget(file, _currentUserID));
+
+    return SafeArea(
+      top: false,
+      child: Scrollbar(
+        thickness: 4,
+        radius: const Radius.circular(2),
+        thumbVisibility: true,
+        child: Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: CustomScrollView(
+            physics: const ClampingScrollPhysics(),
+            shrinkWrap: true,
+            slivers: <Widget>[
+              TitleBarWidget(
+                isFlexibleSpaceDisabled: true,
+                title: "Details",
+                isOnTopOfScreen: false,
+                backgroundColor: getEnteColorScheme(context).backgroundElevated,
+                leading: IconButtonWidget(
+                  icon: Icons.expand_more_outlined,
+                  iconButtonType: IconButtonType.primary,
+                  onTap: () => Navigator.pop(context),
+                ),
+              ),
+              SliverToBoxAdapter(
+                child: AddedByWidget(
+                  widget.file,
+                  _currentUserID,
+                ),
+              ),
+              SliverList(
+                delegate: SliverChildBuilderDelegate(
+                  (context, index) {
+                    return fileDetailsTiles[index];
+                  },
+                  childCount: fileDetailsTiles.length,
+                ),
+              )
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  _generateExifForDetails(Map<String, IfdTag> exif) {
+    if (exif["EXIF FocalLength"] != null) {
+      _exifData["focalLength"] =
+          (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator /
+              (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio)
+                  .denominator;
+    }
+
+    if (exif["EXIF FNumber"] != null) {
+      _exifData["fNumber"] =
+          (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator /
+              (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator;
+    }
+    final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
+    final imageLength = exif["EXIF ExifImageLength"] ??
+        exif["Image "
+            "ImageLength"];
+    if (imageWidth != null && imageLength != null) {
+      _exifData["resolution"] = '$imageWidth x $imageLength';
+      _exifData['megaPixels'] =
+          ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
+                  1000000)
+              .toStringAsFixed(1);
+    } else {
+      debugPrint("No image width/height");
+    }
+    if (exif["Image Make"] != null && exif["Image Model"] != null) {
+      _exifData["takenOnDevice"] =
+          exif["Image Make"].toString() + " " + exif["Image Model"].toString();
+    }
+
+    if (exif["EXIF ExposureTime"] != null) {
+      _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
+    }
+    if (exif["EXIF ISOSpeedRatings"] != null) {
+      _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
+    }
+  }
+}
+
+class FileDetailsDivider extends StatelessWidget {
+  const FileDetailsDivider({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    const dividerPadding = EdgeInsets.symmetric(vertical: 15.5);
+    return const DividerWidget(
+      dividerType: DividerType.menu,
+      divColorHasBlur: false,
+      padding: dividerPadding,
+    );
+  }
+}

+ 0 - 42
lib/ui/viewer/file/file_info_collection_widget.dart

@@ -1,42 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:photos/ente_theme_data.dart';
-
-class FileInfoCollectionWidget extends StatelessWidget {
-  final String? name;
-  final Function? onTap;
-  const FileInfoCollectionWidget({this.name, this.onTap, Key? key})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return GestureDetector(
-      onTap: onTap as void Function()?,
-      child: Container(
-        margin: const EdgeInsets.only(
-          top: 10,
-          bottom: 18,
-          right: 8,
-        ),
-        decoration: BoxDecoration(
-          color: Theme.of(context)
-              .colorScheme
-              .inverseBackgroundColor
-              .withOpacity(0.025),
-          borderRadius: const BorderRadius.all(
-            Radius.circular(8),
-          ),
-        ),
-        child: Center(
-          child: Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 8),
-            child: Text(
-              name!,
-              style: Theme.of(context).textTheme.subtitle2,
-              overflow: TextOverflow.ellipsis,
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-}

+ 0 - 536
lib/ui/viewer/file/file_info_widget.dart

@@ -1,536 +0,0 @@
-import "dart:async";
-
-import "package:exif/exif.dart";
-import "package:flutter/cupertino.dart";
-import "package:flutter/material.dart";
-import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
-import 'package:path/path.dart' as path;
-import 'package:photo_manager/photo_manager.dart';
-import "package:photos/core/configuration.dart";
-import 'package:photos/db/files_db.dart';
-import "package:photos/ente_theme_data.dart";
-import "package:photos/models/file.dart";
-import "package:photos/models/file_type.dart";
-import 'package:photos/services/collections_service.dart';
-import "package:photos/services/feature_flag_service.dart";
-import "package:photos/services/location_service.dart";
-import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/components/divider_widget.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
-import 'package:photos/ui/components/title_bar_widget.dart';
-import 'package:photos/ui/viewer/file/collections_list_of_file_widget.dart';
-import 'package:photos/ui/viewer/file/device_folders_list_of_file_widget.dart';
-import 'package:photos/ui/viewer/file/file_caption_widget.dart';
-import "package:photos/ui/viewer/file/location_chip.dart";
-import "package:photos/ui/viewer/file/locations_list.dart";
-import "package:photos/ui/viewer/file/object_tags_widget.dart";
-import 'package:photos/ui/viewer/file/raw_exif_list_tile_widget.dart';
-import "package:photos/utils/date_time_util.dart";
-import "package:photos/utils/exif_util.dart";
-import "package:photos/utils/file_util.dart";
-import "package:photos/utils/magic_util.dart";
-
-class FileInfoWidget extends StatefulWidget {
-  final File file;
-
-  const FileInfoWidget(
-    this.file, {
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<FileInfoWidget> createState() => _FileInfoWidgetState();
-}
-
-class _FileInfoWidgetState extends State<FileInfoWidget> {
-  Map<String, IfdTag>? _exif;
-  late LocationService locationService = LocationService.instance;
-  final Map<String, dynamic> _exifData = {
-    "focalLength": null,
-    "fNumber": null,
-    "resolution": null,
-    "takenOnDevice": null,
-    "exposureTime": null,
-    "ISO": null,
-    "megaPixels": null
-  };
-
-  bool _isImage = false;
-  int? _currentUserID;
-
-  @override
-  void initState() {
-    debugPrint('file_info_dialog initState');
-    _currentUserID = Configuration.instance.getUserID();
-    _isImage = widget.file.fileType == FileType.image ||
-        widget.file.fileType == FileType.livePhoto;
-    if (_isImage) {
-      getExif(widget.file).then((exif) {
-        if (mounted) {
-          setState(() {
-            _exif = exif;
-          });
-        }
-      });
-    }
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final file = widget.file;
-    final fileIsBackedup = file.uploadedFileID == null ? false : true;
-    final bool isFileOwner =
-        file.ownerID == null || file.ownerID == _currentUserID;
-    late Future<Set<int>> allCollectionIDsOfFile;
-    //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
-    final Future<Set<String>> allDeviceFoldersOfFile =
-        Future.sync(() => {file.deviceFolder ?? ''});
-    if (fileIsBackedup) {
-      allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile(
-        file.uploadedFileID!,
-      );
-    }
-    final dateTime = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
-    final dateTimeForUpdationTime =
-        DateTime.fromMicrosecondsSinceEpoch(file.updationTime!);
-
-    if (_isImage && _exif != null) {
-      _generateExifForDetails(_exif!);
-    }
-    final bool showExifListTile = _exifData["focalLength"] != null ||
-        _exifData["fNumber"] != null ||
-        _exifData["takenOnDevice"] != null ||
-        _exifData["exposureTime"] != null ||
-        _exifData["ISO"] != null;
-    final bool showDimension =
-        _exifData["resolution"] != null && _exifData["megaPixels"] != null;
-    final listTiles = <Widget?>[
-      !widget.file.isUploaded ||
-              (!isFileOwner && (widget.file.caption?.isEmpty ?? true))
-          ? const SizedBox.shrink()
-          : Padding(
-              padding: const EdgeInsets.only(top: 8, bottom: 4),
-              child: isFileOwner
-                  ? FileCaptionWidget(file: widget.file)
-                  : FileCaptionReadyOnly(caption: widget.file.caption!),
-            ),
-      ListTile(
-        horizontalTitleGap: 2,
-        leading: const Padding(
-          padding: EdgeInsets.only(top: 8),
-          child: Icon(Icons.calendar_today_rounded),
-        ),
-        title: Text(
-          getFullDate(
-            DateTime.fromMicrosecondsSinceEpoch(file.creationTime!),
-          ),
-        ),
-        subtitle: Text(
-          getTimeIn12hrFormat(dateTime) + "  " + dateTime.timeZoneName,
-          style: Theme.of(context).textTheme.bodyText2!.copyWith(
-                color: Theme.of(context)
-                    .colorScheme
-                    .defaultTextColor
-                    .withOpacity(0.5),
-              ),
-        ),
-        trailing: (widget.file.ownerID == null ||
-                    widget.file.ownerID == _currentUserID) &&
-                widget.file.uploadedFileID != null
-            ? IconButton(
-                onPressed: () {
-                  _showDateTimePicker(widget.file);
-                },
-                icon: const Icon(Icons.edit),
-              )
-            : const SizedBox.shrink(),
-      ),
-      ListTile(
-        horizontalTitleGap: 2,
-        leading: _isImage
-            ? const Padding(
-                padding: EdgeInsets.only(top: 8),
-                child: Icon(
-                  Icons.image,
-                ),
-              )
-            : const Padding(
-                padding: EdgeInsets.only(top: 8),
-                child: Icon(
-                  Icons.video_camera_back,
-                  size: 27,
-                ),
-              ),
-        title: Text(
-          path.basenameWithoutExtension(file.displayName) +
-              path.extension(file.displayName).toUpperCase(),
-        ),
-        subtitle: Wrap(
-          children: [
-            showDimension
-                ? Text(
-                    "${_exifData["megaPixels"]}MP  "
-                    "${_exifData["resolution"]}  ",
-                  )
-                : const SizedBox.shrink(),
-            _getFileSize(),
-            (file.fileType == FileType.video) &&
-                    (file.localID != null || file.duration != 0)
-                ? Padding(
-                    padding: const EdgeInsets.only(left: 8.0),
-                    child: _getVideoDuration(),
-                  )
-                : const SizedBox.shrink(),
-          ],
-        ),
-        trailing: file.uploadedFileID == null || file.ownerID != _currentUserID
-            ? const SizedBox.shrink()
-            : IconButton(
-                onPressed: () async {
-                  await editFilename(context, file);
-                  setState(() {});
-                },
-                icon: const Icon(Icons.edit),
-              ),
-      ),
-      showExifListTile
-          ? ListTile(
-              horizontalTitleGap: 2,
-              leading: const Icon(Icons.camera_rounded),
-              title: Text(_exifData["takenOnDevice"] ?? "--"),
-              subtitle: Wrap(
-                children: [
-                  _exifData["fNumber"] != null
-                      ? Padding(
-                          padding: const EdgeInsets.only(right: 10),
-                          child: Text('ƒ/' + _exifData["fNumber"].toString()),
-                        )
-                      : const SizedBox.shrink(),
-                  _exifData["exposureTime"] != null
-                      ? Padding(
-                          padding: const EdgeInsets.only(right: 10),
-                          child: Text(_exifData["exposureTime"]),
-                        )
-                      : const SizedBox.shrink(),
-                  _exifData["focalLength"] != null
-                      ? Padding(
-                          padding: const EdgeInsets.only(right: 10),
-                          child:
-                              Text(_exifData["focalLength"].toString() + "mm"),
-                        )
-                      : const SizedBox.shrink(),
-                  _exifData["ISO"] != null
-                      ? Padding(
-                          padding: const EdgeInsets.only(right: 10),
-                          child: Text("ISO" + _exifData["ISO"].toString()),
-                        )
-                      : const SizedBox.shrink(),
-                ],
-              ),
-            )
-          : null,
-      SizedBox(
-        height: 62,
-        child: ListTile(
-          horizontalTitleGap: 0,
-          leading: const Icon(Icons.folder_outlined),
-          title: fileIsBackedup
-              ? CollectionsListOfFileWidget(
-                  allCollectionIDsOfFile,
-                  _currentUserID!,
-                )
-              : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
-        ),
-      ),
-      FeatureFlagService.instance.isInternalUserOrDebugBuild()
-          ? SizedBox(
-              height: 62,
-              child: ListTile(
-                horizontalTitleGap: 0,
-                leading: const Icon(Icons.image_search),
-                title: ObjectTagsWidget(file),
-              ),
-            )
-          : null,
-      (file.uploadedFileID != null && file.updationTime != null)
-          ? ListTile(
-              horizontalTitleGap: 2,
-              leading: const Padding(
-                padding: EdgeInsets.only(top: 8),
-                child: Icon(Icons.cloud_upload_outlined),
-              ),
-              title: Text(
-                getFullDate(
-                  DateTime.fromMicrosecondsSinceEpoch(file.updationTime!),
-                ),
-              ),
-              subtitle: Text(
-                getTimeIn12hrFormat(dateTimeForUpdationTime) +
-                    "  " +
-                    dateTimeForUpdationTime.timeZoneName,
-                style: Theme.of(context).textTheme.bodyText2!.copyWith(
-                      color: Theme.of(context)
-                          .colorScheme
-                          .defaultTextColor
-                          .withOpacity(0.5),
-                    ),
-              ),
-            )
-          : null,
-      ListTile(
-        horizontalTitleGap: 2,
-        leading: Padding(
-          padding: const EdgeInsets.only(top: 8),
-          child: locationService.getLocationsByFileID(file.generatedID!).isEmpty
-              ? const Icon(Icons.add_location_alt_rounded)
-              : const Icon(Icons.location_on_rounded),
-        ),
-        title: Text(
-          locationService.getLocationsByFileID(file.generatedID!).isEmpty
-              ? "Add Location"
-              : "Locations",
-        ),
-        subtitle:
-            locationService.getLocationsByFileID(file.generatedID!).isEmpty
-                ? Text(
-                    "group nearby photos",
-                    style: Theme.of(context).textTheme.bodyMedium!.copyWith(
-                          color: Theme.of(context)
-                              .colorScheme
-                              .defaultTextColor
-                              .withOpacity(0.5),
-                        ),
-                  )
-                : locationChipList(
-                    file.generatedID!,
-                    context,
-                  ),
-        trailing:
-            locationService.getLocationsByFileID(file.generatedID!).isEmpty
-                ? IconButton(
-                    onPressed: () async {
-                      unawaited(
-                        Navigator.of(context).push(
-                          MaterialPageRoute(
-                            builder: (BuildContext context) {
-                              return LocationsList(
-                                state: 1,
-                                fileId: file.generatedID,
-                              );
-                            },
-                          ),
-                        ),
-                      );
-                    },
-                    icon: const Icon(Icons.arrow_forward_ios),
-                  )
-                : const SizedBox.shrink(),
-      ),
-      _isImage ? RawExifListTileWidget(_exif, widget.file) : null,
-    ];
-
-    listTiles.removeWhere(
-      (element) => element == null,
-    );
-
-    return SafeArea(
-      top: false,
-      child: Scrollbar(
-        thickness: 4,
-        radius: const Radius.circular(2),
-        thumbVisibility: true,
-        child: Padding(
-          padding: const EdgeInsets.all(8.0),
-          child: CustomScrollView(
-            physics: const ClampingScrollPhysics(),
-            shrinkWrap: true,
-            slivers: <Widget>[
-              TitleBarWidget(
-                isFlexibleSpaceDisabled: true,
-                title: "Details",
-                isOnTopOfScreen: false,
-                backgroundColor: getEnteColorScheme(context).backgroundElevated,
-                leading: IconButtonWidget(
-                  icon: Icons.close_outlined,
-                  iconButtonType: IconButtonType.primary,
-                  onTap: () => Navigator.pop(context),
-                ),
-              ),
-              SliverToBoxAdapter(child: addedBy(widget.file)),
-              SliverList(
-                delegate: SliverChildBuilderDelegate(
-                  (context, index) {
-                    if (index.isOdd) {
-                      return index == 1
-                          ? const SizedBox.shrink()
-                          : const DividerWidget(
-                              dividerType: DividerType.menu,
-                            );
-                    } else {
-                      return listTiles[index ~/ 2];
-                    }
-                  },
-                  childCount: (listTiles.length * 2) - 1,
-                ),
-              )
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-
-  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);
-      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) {
-    if (exif["EXIF FocalLength"] != null) {
-      _exifData["focalLength"] =
-          (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator /
-              (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio)
-                  .denominator;
-    }
-
-    if (exif["EXIF FNumber"] != null) {
-      _exifData["fNumber"] =
-          (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator /
-              (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator;
-    }
-    final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
-    final imageLength = exif["EXIF ExifImageLength"] ??
-        exif["Image "
-            "ImageLength"];
-    if (imageWidth != null && imageLength != null) {
-      _exifData["resolution"] = '$imageWidth x $imageLength';
-      _exifData['megaPixels'] =
-          ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
-                  1000000)
-              .toStringAsFixed(1);
-    } else {
-      debugPrint("No image width/height");
-    }
-    if (exif["Image Make"] != null && exif["Image Model"] != null) {
-      _exifData["takenOnDevice"] =
-          exif["Image Make"].toString() + " " + exif["Image Model"].toString();
-    }
-
-    if (exif["EXIF ExposureTime"] != null) {
-      _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
-    }
-    if (exif["EXIF ISOSpeedRatings"] != null) {
-      _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
-    }
-  }
-
-  Widget _getFileSize() {
-    Future<int> fileSizeFuture;
-    if (widget.file.fileSize != null) {
-      fileSizeFuture = Future.value(widget.file.fileSize);
-    } else {
-      fileSizeFuture = getFile(widget.file).then((f) => f!.length());
-    }
-    return FutureBuilder<int>(
-      future: fileSizeFuture,
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          return Text(
-            (snapshot.data! / (1024 * 1024)).toStringAsFixed(2) + " MB",
-          );
-        } else {
-          return Center(
-            child: SizedBox.fromSize(
-              size: const Size.square(24),
-              child: const CupertinoActivityIndicator(
-                radius: 8,
-              ),
-            ),
-          );
-        }
-      },
-    );
-  }
-
-  Widget _getVideoDuration() {
-    if (widget.file.duration != 0) {
-      return Text(
-        secondsToHHMMSS(widget.file.duration!),
-      );
-    }
-    return FutureBuilder<AssetEntity?>(
-      future: widget.file.getAsset,
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          return Text(
-            snapshot.data!.videoDuration.toString().split(".")[0],
-          );
-        } else {
-          return Center(
-            child: SizedBox.fromSize(
-              size: const Size.square(24),
-              child: const CupertinoActivityIndicator(
-                radius: 8,
-              ),
-            ),
-          );
-        }
-      },
-    );
-  }
-
-  void _showDateTimePicker(File file) async {
-    final dateResult = await DatePicker.showDatePicker(
-      context,
-      minTime: DateTime(1800, 1, 1),
-      maxTime: DateTime.now(),
-      currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime!),
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateResult == null) {
-      return;
-    }
-    final dateWithTimeResult = await DatePicker.showTime12hPicker(
-      context,
-      showTitleActions: true,
-      currentTime: dateResult,
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateWithTimeResult != null) {
-      if (await editTime(
-        context,
-        List.of([widget.file]),
-        dateWithTimeResult.microsecondsSinceEpoch,
-      )) {
-        widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch;
-        setState(() {});
-      }
-    }
-  }
-}

+ 0 - 77
lib/ui/viewer/file/object_tags_widget.dart

@@ -1,77 +0,0 @@
-import "package:flutter/material.dart";
-import "package:logging/logging.dart";
-import "package:photos/ente_theme_data.dart";
-import "package:photos/models/file.dart";
-import "package:photos/services/object_detection/object_detection_service.dart";
-import "package:photos/ui/common/loading_widget.dart";
-import "package:photos/utils/thumbnail_util.dart";
-
-class ObjectTagsWidget extends StatelessWidget {
-  final File file;
-
-  const ObjectTagsWidget(this.file, {Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FutureBuilder<List<String>>(
-      future: getThumbnail(file).then((data) {
-        return ObjectDetectionService.instance.predict(data!);
-      }),
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final List<String> tags = snapshot.data!;
-          if (tags.isEmpty) {
-            return const ObjectTagWidget("No Results");
-          }
-          return ListView.builder(
-            itemCount: tags.length,
-            scrollDirection: Axis.horizontal,
-            itemBuilder: (context, index) {
-              return ObjectTagWidget(tags[index]);
-            },
-          );
-        } else if (snapshot.hasError) {
-          Logger("ObjectTagsWidget").severe(snapshot.error);
-          return const Icon(Icons.error);
-        } else {
-          return const EnteLoadingWidget();
-        }
-      },
-    );
-  }
-}
-
-class ObjectTagWidget extends StatelessWidget {
-  final String name;
-  const ObjectTagWidget(this.name, {Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      margin: const EdgeInsets.only(
-        top: 10,
-        bottom: 18,
-        right: 8,
-      ),
-      decoration: BoxDecoration(
-        color: Theme.of(context)
-            .colorScheme
-            .inverseBackgroundColor
-            .withOpacity(0.025),
-        borderRadius: const BorderRadius.all(
-          Radius.circular(8),
-        ),
-      ),
-      child: Center(
-        child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 8),
-          child: Text(
-            name!,
-            style: Theme.of(context).textTheme.subtitle2,
-            overflow: TextOverflow.ellipsis,
-          ),
-        ),
-      ),
-    );
-  }
-}

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

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

+ 37 - 0
lib/ui/viewer/file_details/added_by_widget.dart

@@ -0,0 +1,37 @@
+import "package:flutter/material.dart";
+import "package:photos/models/file.dart";
+import "package:photos/services/collections_service.dart";
+import "package:photos/theme/ente_theme.dart";
+
+class AddedByWidget extends StatelessWidget {
+  final File file;
+  final int currentUserID;
+  const AddedByWidget(this.file, this.currentUserID, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    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);
+      addedBy = fileOwner.email;
+    }
+    if (addedBy == null || addedBy.isEmpty) {
+      return const SizedBox.shrink();
+    }
+    return Padding(
+      padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 16),
+      child: Text(
+        "Added by $addedBy",
+        style: getEnteTextTheme(context).miniMuted,
+      ),
+    );
+  }
+}

+ 109 - 0
lib/ui/viewer/file_details/albums_item_widget.dart

@@ -0,0 +1,109 @@
+import "package:flutter/material.dart";
+import "package:logging/logging.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/file.dart";
+import "package:photos/models/gallery_type.dart";
+import "package:photos/services/collections_service.dart";
+import "package:photos/ui/components/buttons/chip_button_widget.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/ui/viewer/gallery/collection_page.dart";
+import "package:photos/utils/navigation_util.dart";
+
+class AlbumsItemWidget extends StatelessWidget {
+  final File file;
+  final int currentUserID;
+  const AlbumsItemWidget(
+    this.file,
+    this.currentUserID, {
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final fileIsBackedup = file.uploadedFileID == null ? false : true;
+    late Future<Set<int>> allCollectionIDsOfFile;
+    //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
+    final Future<Set<String>> allDeviceFoldersOfFile =
+        Future.sync(() => {file.deviceFolder ?? ''});
+    if (fileIsBackedup) {
+      allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile(
+        file.uploadedFileID!,
+      );
+    }
+    return InfoItemWidget(
+      key: const ValueKey("Albums"),
+      leadingIcon: Icons.folder_outlined,
+      title: "Albums",
+      subtitleSection: fileIsBackedup
+          ? _collectionsListOfFile(
+              context,
+              allCollectionIDsOfFile,
+              currentUserID,
+            )
+          : _deviceFoldersListOfFile(allDeviceFoldersOfFile),
+      hasChipButtons: true,
+    );
+  }
+
+  Future<List<ChipButtonWidget>> _deviceFoldersListOfFile(
+    Future<Set<String>> allDeviceFoldersOfFile,
+  ) async {
+    try {
+      final chipButtons = <ChipButtonWidget>[];
+      final List<String> deviceFolders =
+          (await allDeviceFoldersOfFile).toList();
+      for (var deviceFolder in deviceFolders) {
+        chipButtons.add(
+          ChipButtonWidget(
+            deviceFolder,
+          ),
+        );
+      }
+      return chipButtons;
+    } catch (e, s) {
+      Logger("AlbumsItemWidget").info(e, s);
+      return [];
+    }
+  }
+
+  Future<List<ChipButtonWidget>> _collectionsListOfFile(
+    BuildContext context,
+    Future<Set<int>> allCollectionIDsOfFile,
+    int currentUserID,
+  ) async {
+    try {
+      final chipButtons = <ChipButtonWidget>[];
+      final Set<int> collectionIDs = await allCollectionIDsOfFile;
+      final collections = <Collection>[];
+      for (var collectionID in collectionIDs) {
+        final c = CollectionsService.instance.getCollectionByID(collectionID);
+        collections.add(c!);
+        chipButtons.add(
+          ChipButtonWidget(
+            c.isHidden() ? "Hidden" : c.name,
+            onTap: () {
+              if (c.isHidden()) {
+                return;
+              }
+              routeToPage(
+                context,
+                CollectionPage(
+                  CollectionWithThumbnail(c, null),
+                  appBarType: c.isOwner(currentUserID)
+                      ? GalleryType.ownedCollection
+                      : GalleryType.sharedCollection,
+                ),
+              );
+            },
+          ),
+        );
+      }
+      return chipButtons;
+    } catch (e, s) {
+      Logger("AlbumsItemWidget").info(e, s);
+      return [];
+    }
+  }
+}

+ 31 - 0
lib/ui/viewer/file_details/backed_up_time_item_widget.dart

@@ -0,0 +1,31 @@
+import "package:flutter/material.dart";
+import "package:photos/models/file.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/utils/date_time_util.dart";
+
+class BackedUpTimeItemWidget extends StatelessWidget {
+  final File file;
+  const BackedUpTimeItemWidget(this.file, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final dateTimeForUpdationTime =
+        DateTime.fromMicrosecondsSinceEpoch(file.updationTime!);
+    return InfoItemWidget(
+      key: const ValueKey("Backedup date"),
+      leadingIcon: Icons.backup_outlined,
+      title: getFullDate(
+        DateTime.fromMicrosecondsSinceEpoch(file.updationTime!),
+      ),
+      subtitleSection: Future.value([
+        Text(
+          getTimeIn12hrFormat(dateTimeForUpdationTime) +
+              "  " +
+              dateTimeForUpdationTime.timeZoneName,
+          style: getEnteTextTheme(context).smallMuted,
+        ),
+      ]),
+    );
+  }
+}

+ 76 - 0
lib/ui/viewer/file_details/creation_time_item_widget.dart

@@ -0,0 +1,76 @@
+import "package:flutter/material.dart";
+import "package:flutter_datetime_picker/flutter_datetime_picker.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/models/file.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/utils/date_time_util.dart";
+import "package:photos/utils/magic_util.dart";
+
+class CreationTimeItem extends StatefulWidget {
+  final File file;
+  final int currentUserID;
+  const CreationTimeItem(this.file, this.currentUserID, {super.key});
+
+  @override
+  State<CreationTimeItem> createState() => _CreationTimeItemState();
+}
+
+class _CreationTimeItemState extends State<CreationTimeItem> {
+  @override
+  Widget build(BuildContext context) {
+    final dateTime =
+        DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!);
+    return InfoItemWidget(
+      key: const ValueKey("Creation time"),
+      leadingIcon: Icons.calendar_today_outlined,
+      title: getFullDate(
+        DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!),
+      ),
+      subtitleSection: Future.value([
+        Text(
+          getTimeIn12hrFormat(dateTime) + "  " + dateTime.timeZoneName,
+          style: getEnteTextTheme(context).smallMuted,
+        ),
+      ]),
+      editOnTap: ((widget.file.ownerID == null ||
+                  widget.file.ownerID == widget.currentUserID) &&
+              widget.file.uploadedFileID != null)
+          ? () {
+              _showDateTimePicker(widget.file);
+            }
+          : null,
+    );
+  }
+
+  void _showDateTimePicker(File file) async {
+    final dateResult = await DatePicker.showDatePicker(
+      context,
+      minTime: DateTime(1800, 1, 1),
+      maxTime: DateTime.now(),
+      currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime!),
+      locale: LocaleType.en,
+      theme: Theme.of(context).colorScheme.dateTimePickertheme,
+    );
+    if (dateResult == null) {
+      return;
+    }
+    final dateWithTimeResult = await DatePicker.showTime12hPicker(
+      context,
+      showTitleActions: true,
+      currentTime: dateResult,
+      locale: LocaleType.en,
+      theme: Theme.of(context).colorScheme.dateTimePickertheme,
+    );
+    if (dateWithTimeResult != null) {
+      if (await editTime(
+        context,
+        List.of([widget.file]),
+        dateWithTimeResult.microsecondsSinceEpoch,
+      )) {
+        widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch;
+        setState(() {});
+      }
+    }
+  }
+}

+ 96 - 0
lib/ui/viewer/file_details/exif_item_widgets.dart

@@ -0,0 +1,96 @@
+import "package:exif/exif.dart";
+import "package:flutter/material.dart";
+import "package:photos/models/file.dart";
+import "package:photos/theme/colors.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/buttons/inline_button_widget.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/ui/viewer/file/exif_info_dialog.dart";
+import "package:photos/utils/toast_util.dart";
+
+class BasicExifItemWidget extends StatelessWidget {
+  final Map<String, dynamic> exifData;
+  const BasicExifItemWidget(this.exifData, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final subtitleTextTheme = getEnteTextTheme(context).smallMuted;
+    return InfoItemWidget(
+      key: const ValueKey("Basic EXIF"),
+      leadingIcon: Icons.camera_outlined,
+      title: exifData["takenOnDevice"] ?? "--",
+      subtitleSection: Future.value([
+        if (exifData["fNumber"] != null)
+          Text(
+            'ƒ/' + exifData["fNumber"].toString(),
+            style: subtitleTextTheme,
+          ),
+        if (exifData["exposureTime"] != null)
+          Text(
+            exifData["exposureTime"],
+            style: subtitleTextTheme,
+          ),
+        if (exifData["focalLength"] != null)
+          Text(
+            exifData["focalLength"].toString() + "mm",
+            style: subtitleTextTheme,
+          ),
+        if (exifData["ISO"] != null)
+          Text(
+            "ISO" + exifData["ISO"].toString(),
+            style: subtitleTextTheme,
+          ),
+      ]),
+    );
+  }
+}
+
+class AllExifItemWidget extends StatelessWidget {
+  final File file;
+  final Map<String, IfdTag>? exif;
+  const AllExifItemWidget(
+    this.file,
+    this.exif, {
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return InfoItemWidget(
+      leadingIcon: Icons.text_snippet_outlined,
+      title: "EXIF",
+      subtitleSection: _exifButton(context, file, exif),
+    );
+  }
+
+  Future<List<InlineButtonWidget>> _exifButton(
+    BuildContext context,
+    File file,
+    Map<String, IfdTag>? exif,
+  ) {
+    late final String label;
+    late final VoidCallback? onTap;
+    if (exif == null) {
+      label = "Loading EXIF data...";
+      onTap = null;
+    } else if (exif.isNotEmpty) {
+      label = "View all EXIF data";
+      onTap = () => showDialog(
+            context: context,
+            builder: (BuildContext context) {
+              return ExifInfoDialog(file);
+            },
+            barrierColor: backdropFaintDark,
+          );
+    } else {
+      label = "No EXIF data";
+      onTap = () => showShortToast(context, "This image has no exif data");
+    }
+    return Future.value([
+      InlineButtonWidget(
+        label,
+        onTap,
+      )
+    ]);
+  }
+}

+ 99 - 0
lib/ui/viewer/file_details/file_properties_item_widget.dart

@@ -0,0 +1,99 @@
+import "package:flutter/material.dart";
+import 'package:path/path.dart' as path;
+import "package:photos/models/file.dart";
+import "package:photos/models/file_type.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/utils/date_time_util.dart";
+import "package:photos/utils/file_util.dart";
+import "package:photos/utils/magic_util.dart";
+
+class FilePropertiesItemWidget extends StatefulWidget {
+  final File file;
+  final bool isImage;
+  final Map<String, dynamic> exifData;
+  final int currentUserID;
+  const FilePropertiesItemWidget(
+    this.file,
+    this.isImage,
+    this.exifData,
+    this.currentUserID, {
+    super.key,
+  });
+  @override
+  State<FilePropertiesItemWidget> createState() =>
+      _FilePropertiesItemWidgetState();
+}
+
+class _FilePropertiesItemWidgetState extends State<FilePropertiesItemWidget> {
+  @override
+  Widget build(BuildContext context) {
+    return InfoItemWidget(
+      key: const ValueKey("File properties"),
+      leadingIcon: widget.isImage
+          ? Icons.photo_outlined
+          : Icons.video_camera_back_outlined,
+      title: path.basenameWithoutExtension(widget.file.displayName) +
+          path.extension(widget.file.displayName).toUpperCase(),
+      subtitleSection: _subTitleSection(),
+      editOnTap: widget.file.uploadedFileID == null ||
+              widget.file.ownerID != widget.currentUserID
+          ? null
+          : () async {
+              await editFilename(context, widget.file);
+              setState(() {});
+            },
+    );
+  }
+
+  Future<List<Widget>> _subTitleSection() async {
+    final bool showDimension = widget.exifData["resolution"] != null &&
+        widget.exifData["megaPixels"] != null;
+    final subSectionWidgets = <Widget>[];
+
+    if (showDimension) {
+      subSectionWidgets.add(
+        Text(
+          "${widget.exifData["megaPixels"]}MP  "
+          "${widget.exifData["resolution"]}  ",
+          style: getEnteTextTheme(context).smallMuted,
+        ),
+      );
+    }
+
+    int fileSize;
+    if (widget.file.fileSize != null) {
+      fileSize = widget.file.fileSize!;
+    } else {
+      fileSize = await getFile(widget.file).then((f) => f!.length());
+    }
+    subSectionWidgets.add(
+      Text(
+        (fileSize / (1024 * 1024)).toStringAsFixed(2) + " MB",
+        style: getEnteTextTheme(context).smallMuted,
+      ),
+    );
+
+    if ((widget.file.fileType == FileType.video) &&
+        (widget.file.localID != null || widget.file.duration != 0)) {
+      if (widget.file.duration != 0) {
+        subSectionWidgets.add(
+          Text(
+            secondsToHHMMSS(widget.file.duration!),
+            style: getEnteTextTheme(context).smallMuted,
+          ),
+        );
+      } else {
+        final asset = await widget.file.getAsset;
+        subSectionWidgets.add(
+          Text(
+            asset?.videoDuration.toString().split(".")[0] ?? "",
+            style: getEnteTextTheme(context).smallMuted,
+          ),
+        );
+      }
+    }
+
+    return Future.value(subSectionWidgets);
+  }
+}

+ 49 - 0
lib/ui/viewer/file_details/objects_item_widget.dart

@@ -0,0 +1,49 @@
+import "package:flutter/material.dart";
+import "package:logging/logging.dart";
+import "package:photos/models/file.dart";
+import "package:photos/services/object_detection/object_detection_service.dart";
+import "package:photos/ui/components/buttons/chip_button_widget.dart";
+import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/utils/thumbnail_util.dart";
+
+class ObjectsItemWidget extends StatelessWidget {
+  final File file;
+  const ObjectsItemWidget(this.file, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return InfoItemWidget(
+      key: const ValueKey("Objects"),
+      leadingIcon: Icons.image_search_outlined,
+      title: "Objects",
+      subtitleSection: _objectTags(file),
+      hasChipButtons: true,
+    );
+  }
+
+  Future<List<ChipButtonWidget>> _objectTags(File file) async {
+    try {
+      final chipButtons = <ChipButtonWidget>[];
+      var objectTags = <String>[];
+      final thumbnail = await getThumbnail(file);
+      if (thumbnail != null) {
+        objectTags = await ObjectDetectionService.instance.predict(thumbnail);
+      }
+      if (objectTags.isEmpty) {
+        return const [
+          ChipButtonWidget(
+            "No results",
+            noChips: true,
+          )
+        ];
+      }
+      for (String objectTag in objectTags) {
+        chipButtons.add(ChipButtonWidget(objectTag));
+      }
+      return chipButtons;
+    } catch (e, s) {
+      Logger("ObjctsItemWidget").info(e, s);
+      return [];
+    }
+  }
+}

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

@@ -18,7 +18,7 @@ import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/sharing/album_participants_page.dart';

+ 1 - 1
lib/ui/viewer/search/search_widget.dart

@@ -5,7 +5,7 @@ import 'package:logging/logging.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/search/search_result.dart';
 import 'package:photos/services/search_service.dart';
-import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/buttons/icon_button_widget.dart';
 import 'package:photos/ui/viewer/search/result/no_result_widget.dart';
 import 'package:photos/ui/viewer/search/search_suffix_icon_widget.dart';
 import 'package:photos/ui/viewer/search/search_suggestions.dart';

+ 0 - 14
lib/utils/auth_util.dart

@@ -1,4 +1,3 @@
-import 'package:local_auth/auth_strings.dart';
 import 'package:local_auth/local_auth.dart';
 import 'package:logging/logging.dart';
 
@@ -7,18 +6,5 @@ Future<bool> requestAuthentication(String reason) async {
   await LocalAuthentication().stopAuthentication();
   return await LocalAuthentication().authenticate(
     localizedReason: reason,
-    androidAuthStrings: const AndroidAuthMessages(
-      biometricHint: "Verify identity",
-      biometricNotRecognized: "Not recognized, try again",
-      biometricRequiredTitle: "Biometric required",
-      biometricSuccess: "Successfully verified",
-      cancelButton: "Cancel",
-      deviceCredentialsRequiredTitle: "Device credentials required",
-      deviceCredentialsSetupDescription: "Device credentials required",
-      goToSettingsButton: "Go to settings",
-      goToSettingsDescription:
-          "Authentication is not setup on your device, go to Settings > Security to set it up",
-      signInTitle: "Authentication required",
-    ),
   );
 }

+ 1 - 1
lib/utils/delete_file_util.dart

@@ -23,7 +23,7 @@ import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/trash_sync_service.dart';
 import 'package:photos/ui/common/linear_progress_dialog.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';

+ 1 - 1
lib/utils/dialog_util.dart

@@ -10,7 +10,7 @@ import 'package:photos/theme/colors.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/ui/components/action_sheet_widget.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 

+ 1 - 1
lib/utils/email_util.dart

@@ -12,7 +12,7 @@ import 'package:package_info_plus/package_info_plus.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/error-reporting/super_logging.dart';
-import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/buttons/button_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/tools/debug/log_file_viewer.dart';

+ 34 - 2
pubspec.lock

@@ -904,10 +904,42 @@ packages:
     dependency: "direct main"
     description:
       name: local_auth
-      sha256: d3fece0749101725b03206f84a7dab7aaafb702dbbd09131ff8d8173259a9b19
+      sha256: "0cf238be2bfa51a6c9e7e9cfc11c05ea39f2a3a4d3e5bb255d0ebc917da24401"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.11"
+    version: "2.1.6"
+  local_auth_android:
+    dependency: transitive
+    description:
+      name: local_auth_android
+      sha256: "2ccfadbb6fbc63e6674ad58a350b06188829e62669d67a0c752c4e43cb88272a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.21"
+  local_auth_ios:
+    dependency: transitive
+    description:
+      name: local_auth_ios
+      sha256: "604078f6492fe7730fc5bb8e4f2cfe2bc287a9b499ea0ff30a29925fc1873728"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  local_auth_platform_interface:
+    dependency: transitive
+    description:
+      name: local_auth_platform_interface
+      sha256: "9e160d59ef0743e35f1b50f4fb84dc64f55676b1b8071e319ef35e7f3bc13367"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.7"
+  local_auth_windows:
+    dependency: transitive
+    description:
+      name: local_auth_windows
+      sha256: bfe0deede77fb36faa62799977074f35ac096d7cafce0c29a44a173d2a2a4b94
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.7"
   logging:
     dependency: "direct main"
     description:

+ 2 - 2
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
-version: 0.7.31+431
+version: 0.7.33+433
 
 environment:
   sdk: '>=2.17.0 <3.0.0'
@@ -75,7 +75,7 @@ dependencies:
   intl: ^0.17.0
   like_button: ^2.0.2
   loading_animations: ^2.1.0
-  local_auth: ^1.1.5
+  local_auth: ^2.1.5
   logging: ^1.0.1
   lottie: ^1.2.2
   media_extension: ^1.0.1