Преглед изворни кода

Merge branch 'main' into replace-dialogs

ashilkn пре 2 година
родитељ
комит
fe7a1ce556
37 измењених фајлова са 887 додато и 197 уклоњено
  1. 4 0
      lib/models/collection.dart
  2. 16 0
      lib/models/gallery_type.dart
  3. 2 0
      lib/models/magic_metadata.dart
  4. 23 2
      lib/services/collections_service.dart
  5. 24 12
      lib/services/hidden_service.dart
  6. 3 3
      lib/services/user_service.dart
  7. 1 1
      lib/ui/account/recovery_key_page.dart
  8. 1 1
      lib/ui/account/recovery_page.dart
  9. 1 1
      lib/ui/account/two_factor_setup_page.dart
  10. 51 1
      lib/ui/actions/collection/collection_sharing_actions.dart
  11. 22 2
      lib/ui/advanced_settings_screen.dart
  12. 1 1
      lib/ui/collections/collection_item_widget.dart
  13. 1 1
      lib/ui/collections/device_folders_grid_view_widget.dart
  14. 17 7
      lib/ui/collections_gallery_widget.dart
  15. 28 1
      lib/ui/components/action_sheet_widget.dart
  16. 110 104
      lib/ui/components/button_widget.dart
  17. 1 1
      lib/ui/components/captioned_text_widget.dart
  18. 6 1
      lib/ui/components/dialog_widget.dart
  19. 5 0
      lib/ui/create_collection_page.dart
  20. 1 1
      lib/ui/payment/stripe_subscription_page.dart
  21. 1 1
      lib/ui/payment/subscription_page.dart
  22. 2 1
      lib/ui/settings/about_section_widget.dart
  23. 1 1
      lib/ui/settings/backup_section_widget.dart
  24. 2 2
      lib/ui/settings/debug_section_widget.dart
  25. 1 3
      lib/ui/settings_page.dart
  26. 13 3
      lib/ui/shared_collections_gallery.dart
  27. 1 1
      lib/ui/sharing/manage_links_widget.dart
  28. 1 1
      lib/ui/sharing/share_collection_page.dart
  29. 192 0
      lib/ui/tools/debug/app_storage_viewer.dart
  30. 119 0
      lib/ui/tools/debug/path_storage_viewer.dart
  31. 26 23
      lib/ui/tools/editor/image_editor_page.dart
  32. 121 12
      lib/ui/viewer/actions/file_selection_actions_widget.dart
  33. 1 1
      lib/ui/viewer/file/fading_app_bar.dart
  34. 1 2
      lib/ui/viewer/file/zoomable_live_image.dart
  35. 29 3
      lib/utils/date_time_util.dart
  36. 55 0
      lib/utils/directory_content.dart
  37. 3 3
      lib/utils/magic_util.dart

+ 4 - 0
lib/models/collection.dart

@@ -58,6 +58,10 @@ class Collection {
     return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
     return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
   }
   }
 
 
+  bool isSharedFilesCollection() {
+    return (magicMetadata.subType ?? 0) == subTypeSharedFilesCollection;
+  }
+
   List<User> getSharees() {
   List<User> getSharees() {
     final List<User> result = [];
     final List<User> result = [];
     if (sharees == null) {
     if (sharees == null) {

+ 16 - 0
lib/models/gallery_type.dart

@@ -80,6 +80,22 @@ extension GalleyTypeExtension on GalleryType {
     }
     }
   }
   }
 
 
+  bool showCreateLink() {
+    switch (this) {
+      case GalleryType.ownedCollection:
+      case GalleryType.searchResults:
+      case GalleryType.homepage:
+      case GalleryType.favorite:
+      case GalleryType.archive:
+        return true;
+      case GalleryType.hidden:
+      case GalleryType.localFolder:
+      case GalleryType.trash:
+      case GalleryType.sharedCollection:
+        return false;
+    }
+  }
+
   bool showRemoveFromAlbum() {
   bool showRemoveFromAlbum() {
     switch (this) {
     switch (this) {
       case GalleryType.ownedCollection:
       case GalleryType.ownedCollection:

+ 2 - 0
lib/models/magic_metadata.dart

@@ -7,6 +7,7 @@ const visibilityHidden = 2;
 
 
 // Collection SubType Constants
 // Collection SubType Constants
 const subTypeDefaultHidden = 1;
 const subTypeDefaultHidden = 1;
+const subTypeSharedFilesCollection = 2;
 
 
 const magicKeyVisibility = 'visibility';
 const magicKeyVisibility = 'visibility';
 // key for collection subType
 // key for collection subType
@@ -76,6 +77,7 @@ class CollectionMagicMetadata {
 
 
   // null/0 value -> no subType
   // null/0 value -> no subType
   // 1 -> DEFAULT_HIDDEN COLLECTION for files hidden individually
   // 1 -> DEFAULT_HIDDEN COLLECTION for files hidden individually
+  // 2 -> Collections created for sharing selected files
   int? subType;
   int? subType;
 
 
   CollectionMagicMetadata({required this.visibility, this.subType});
   CollectionMagicMetadata({required this.visibility, this.subType});

+ 23 - 2
lib/services/collections_service.dart

@@ -457,6 +457,11 @@ class CollectionsService {
 
 
   Future<void> rename(Collection collection, String newName) async {
   Future<void> rename(Collection collection, String newName) async {
     try {
     try {
+      // Note: when collection created to sharing few files is renamed
+      // convert that collection to a regular collection type.
+      if (collection.isSharedFilesCollection()) {
+        await updateMagicMetadata(collection, {"subType": 0});
+      }
       final encryptedName = CryptoUtil.encryptSync(
       final encryptedName = CryptoUtil.encryptSync(
         utf8.encode(newName),
         utf8.encode(newName),
         getCollectionKey(collection.id),
         getCollectionKey(collection.id),
@@ -1029,8 +1034,24 @@ class CollectionsService {
       "/collections",
       "/collections",
       data: payload,
       data: payload,
     )
     )
-        .then((response) {
-      final collection = Collection.fromMap(response.data["collection"]);
+        .then((response) async {
+      final collectionData = response.data["collection"];
+      final collection = Collection.fromMap(collectionData);
+      if (createRequest != null) {
+        if (collectionData['magicMetadata'] != null) {
+          final decryptionKey = _getAndCacheDecryptedKey(collection);
+          final utfEncodedMmd = await CryptoUtil.decryptChaCha(
+            Sodium.base642bin(collectionData['magicMetadata']['data']),
+            decryptionKey,
+            Sodium.base642bin(collectionData['magicMetadata']['header']),
+          );
+          collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
+          collection.mMdVersion = collectionData['magicMetadata']['version'];
+          collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
+            collection.mMdEncodedJson,
+          );
+        }
+      }
       return _cacheCollectionAttributes(collection);
       return _cacheCollectionAttributes(collection);
     });
     });
   }
   }

+ 24 - 12
lib/services/hidden_service.dart

@@ -100,15 +100,35 @@ extension HiddenService on CollectionsService {
   }
   }
 
 
   Future<Collection> _createDefaultHiddenAlbum() async {
   Future<Collection> _createDefaultHiddenAlbum() async {
+    final CreateRequest createRequest = await buildCollectionCreateRequest(
+      ".Hidden",
+      visibility: visibilityHidden,
+      subType: subTypeDefaultHidden,
+    );
+    _logger.info("Creating Hidden Collection");
+    final collection =
+        await createAndCacheCollection(null, createRequest: createRequest);
+    _logger.info("Creating Hidden Collection Created Successfully");
+    final Collection collectionFromServer =
+        await fetchCollectionByID(collection.id);
+    _logger.info("Fetched Created Hidden Collection Successfully");
+    return collectionFromServer;
+  }
+
+  Future<CreateRequest> buildCollectionCreateRequest(
+    String name, {
+    required int visibility,
+    required int subType,
+  }) async {
     final key = CryptoUtil.generateKey();
     final key = CryptoUtil.generateKey();
     final encryptedKeyData = CryptoUtil.encryptSync(key, config.getKey()!);
     final encryptedKeyData = CryptoUtil.encryptSync(key, config.getKey()!);
     final encryptedName = CryptoUtil.encryptSync(
     final encryptedName = CryptoUtil.encryptSync(
-      utf8.encode(".Hidden") as Uint8List,
+      utf8.encode(name) as Uint8List,
       key,
       key,
     );
     );
     final jsonToUpdate = CollectionMagicMetadata(
     final jsonToUpdate = CollectionMagicMetadata(
-      visibility: visibilityHidden,
-      subType: subTypeDefaultHidden,
+      visibility: visibility,
+      subType: subType,
     ).toJson();
     ).toJson();
     assert(jsonToUpdate.length == 2, "metadata should have two keys");
     assert(jsonToUpdate.length == 2, "metadata should have two keys");
     final encryptedMMd = await CryptoUtil.encryptChaCha(
     final encryptedMMd = await CryptoUtil.encryptChaCha(
@@ -130,14 +150,6 @@ extension HiddenService on CollectionsService {
       attributes: CollectionAttributes(),
       attributes: CollectionAttributes(),
       magicMetadata: metadataRequest,
       magicMetadata: metadataRequest,
     );
     );
-
-    _logger.info("Creating Hidden Collection");
-    final collection =
-        await createAndCacheCollection(null, createRequest: createRequest);
-    _logger.info("Creating Hidden Collection Created Successfully");
-    final Collection collectionFromServer =
-        await fetchCollectionByID(collection.id);
-    _logger.info("Fetched Created Hidden Collection Successfully");
-    return collectionFromServer;
+    return createRequest;
   }
   }
 }
 }

+ 3 - 3
lib/services/user_service.dart

@@ -332,7 +332,7 @@ class UserService {
       );
       );
       await dialog.hide();
       await dialog.hide();
       if (response != null && response.statusCode == 200) {
       if (response != null && response.statusCode == 200) {
-        showToast(context, "Email changed to " + email);
+        showShortToast(context, "Email changed to " + email);
         await setEmail(email);
         await setEmail(email);
         Navigator.of(context).popUntil((route) => route.isFirst);
         Navigator.of(context).popUntil((route) => route.isFirst);
         Bus.instance.fire(UserDetailsChangedEvent());
         Bus.instance.fire(UserDetailsChangedEvent());
@@ -432,7 +432,7 @@ class UserService {
       );
       );
       await dialog.hide();
       await dialog.hide();
       if (response != null && response.statusCode == 200) {
       if (response != null && response.statusCode == 200) {
-        showToast(context, "Authentication successful!");
+        showShortToast(context, "Authentication successful!");
         await _saveConfiguration(response);
         await _saveConfiguration(response);
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
@@ -703,7 +703,7 @@ class UserService {
       await dialog.hide();
       await dialog.hide();
       Bus.instance.fire(TwoFactorStatusChangeEvent(false));
       Bus.instance.fire(TwoFactorStatusChangeEvent(false));
       unawaited(
       unawaited(
-        showToast(
+        showShortToast(
           context,
           context,
           "Two-factor authentication has been disabled",
           "Two-factor authentication has been disabled",
         ),
         ),

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

@@ -134,7 +134,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                                   await Clipboard.setData(
                                   await Clipboard.setData(
                                     ClipboardData(text: recoveryKey),
                                     ClipboardData(text: recoveryKey),
                                   );
                                   );
-                                  showToast(
+                                  showShortToast(
                                     context,
                                     context,
                                     "Recovery key copied to clipboard",
                                     "Recovery key copied to clipboard",
                                   );
                                   );

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

@@ -53,7 +53,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
           try {
           try {
             await Configuration.instance.recover(_recoveryKey.text.trim());
             await Configuration.instance.recover(_recoveryKey.text.trim());
             await dialog.hide();
             await dialog.hide();
-            showToast(context, "Recovery successful!");
+            showShortToast(context, "Recovery successful!");
             Navigator.of(context).pushReplacement(
             Navigator.of(context).pushReplacement(
               MaterialPageRoute(
               MaterialPageRoute(
                 builder: (BuildContext context) {
                 builder: (BuildContext context) {

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

@@ -139,7 +139,7 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
     return GestureDetector(
     return GestureDetector(
       onTap: () async {
       onTap: () async {
         await Clipboard.setData(ClipboardData(text: widget.secretCode));
         await Clipboard.setData(ClipboardData(text: widget.secretCode));
-        showToast(context, "Code copied to clipboard");
+        showShortToast(context, "Code copied to clipboard");
       },
       },
       child: Column(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         mainAxisAlignment: MainAxisAlignment.center,

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

@@ -2,13 +2,18 @@ import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/api/collection/create_request.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/hidden_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/components/dialog_widget.dart';
 import 'package:photos/ui/payment/subscription.dart';
 import 'package:photos/ui/payment/subscription.dart';
+import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/email_util.dart';
 import 'package:photos/utils/email_util.dart';
 import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/share_util.dart';
@@ -62,6 +67,51 @@ class CollectionActions {
     }
     }
   }
   }
 
 
+  Future<Collection?> createSharedCollectionLink(
+    BuildContext context,
+    List<File> files,
+  ) async {
+    final dialog =
+        createProgressDialog(context, "Creating link...", isDismissible: true);
+    dialog.show();
+    try {
+      // create album with emptyName, use collectionCreationTime on UI to
+      // show name
+      logger.finest("creating album for sharing files");
+      final File fileWithMinCreationTime = files.reduce(
+        (a, b) => (a.creationTime ?? 0) < (b.creationTime ?? 0) ? a : b,
+      );
+      final File fileWithMaxCreationTime = files.reduce(
+        (a, b) => (a.creationTime ?? 0) > (b.creationTime ?? 0) ? a : b,
+      );
+      final String dummyName = getNameForDateRange(
+        fileWithMinCreationTime.creationTime!,
+        fileWithMaxCreationTime.creationTime!,
+      );
+      final CreateRequest req =
+          await collectionsService.buildCollectionCreateRequest(
+        dummyName,
+        visibility: visibilityVisible,
+        subType: subTypeSharedFilesCollection,
+      );
+      final collection = await collectionsService.createAndCacheCollection(
+        null,
+        createRequest: req,
+      );
+      logger.finest("adding files to share to new album");
+      await collectionsService.addToCollection(collection.id, files);
+      logger.finest("creating public link for the newly created album");
+      await CollectionsService.instance.createShareUrl(collection);
+      dialog.hide();
+      return collection;
+    } catch (e, s) {
+      dialog.hide();
+      showGenericErrorDialog(context);
+      logger.severe("Failing to create link for selected files", e, s);
+    }
+    return null;
+  }
+
   // removeParticipant remove the user from a share album
   // removeParticipant remove the user from a share album
   Future<bool?> removeParticipant(
   Future<bool?> removeParticipant(
     BuildContext context,
     BuildContext context,
@@ -88,7 +138,7 @@ class CollectionActions {
           await CollectionsService.instance.unshare(collection.id, user.email);
           await CollectionsService.instance.unshare(collection.id, user.email);
       collection.updateSharees(newSharees);
       collection.updateSharees(newSharees);
       await dialog.hide();
       await dialog.hide();
-      showToast(context, "Stopped sharing with " + user.email + ".");
+      showShortToast(context, "Stopped sharing with " + user.email + ".");
       return true;
       return true;
     } catch (e, s) {
     } catch (e, s) {
       Logger("EmailItemWidget").severe(e, s);
       Logger("EmailItemWidget").severe(e, s);

+ 22 - 2
lib/ui/advanced_settings_screen.dart

@@ -9,7 +9,9 @@ import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/components/title_bar_widget.dart';
 import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/ui/tools/debug/app_storage_viewer.dart';
 import 'package:photos/utils/local_settings.dart';
 import 'package:photos/utils/local_settings.dart';
+import 'package:photos/utils/navigation_util.dart';
 
 
 class AdvancedSettingsScreen extends StatefulWidget {
 class AdvancedSettingsScreen extends StatefulWidget {
   const AdvancedSettingsScreen({super.key});
   const AdvancedSettingsScreen({super.key});
@@ -73,8 +75,8 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
                                 ),
                                 ),
                                 menuItemColor: colorScheme.fillFaint,
                                 menuItemColor: colorScheme.fillFaint,
                                 trailingWidget: Icon(
                                 trailingWidget: Icon(
-                                  Icons.chevron_right,
-                                  color: colorScheme.strokeMuted,
+                                  Icons.chevron_right_outlined,
+                                  color: colorScheme.strokeBase,
                                 ),
                                 ),
                                 borderRadius: 8,
                                 borderRadius: 8,
                                 alignCaptionedTextToLeft: true,
                                 alignCaptionedTextToLeft: true,
@@ -82,6 +84,24 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
                                 isGestureDetectorDisabled: true,
                                 isGestureDetectorDisabled: true,
                               ),
                               ),
                             ),
                             ),
+                            const SizedBox(
+                              height: 24,
+                            ),
+                            MenuItemWidget(
+                              captionedTextWidget: const CaptionedTextWidget(
+                                title: "Manage device storage",
+                              ),
+                              menuItemColor: colorScheme.fillFaint,
+                              trailingWidget: Icon(
+                                Icons.chevron_right_outlined,
+                                color: colorScheme.strokeBase,
+                              ),
+                              borderRadius: 8,
+                              alignCaptionedTextToLeft: true,
+                              onTap: () {
+                                routeToPage(context, const AppStorageViewer());
+                              },
+                            ),
                           ],
                           ],
                         ),
                         ),
                       ],
                       ],

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

@@ -56,7 +56,7 @@ class CollectionItem extends StatelessWidget {
             children: [
             children: [
               const SizedBox(height: 2),
               const SizedBox(height: 2),
               Text(
               Text(
-                c.collection.name ?? "Unnamed",
+                (c.collection.name ?? "Unnamed").trim(),
                 style: enteTextTheme.small,
                 style: enteTextTheme.small,
                 overflow: TextOverflow.ellipsis,
                 overflow: TextOverflow.ellipsis,
               ),
               ),

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

@@ -87,7 +87,7 @@ class _DeviceFoldersGridViewWidgetState
                         itemCount: snapshot.data.length,
                         itemCount: snapshot.data.length,
                       );
                       );
               } else if (snapshot.hasError) {
               } else if (snapshot.hasError) {
-                logger.severe("failed to load device galler", snapshot.error);
+                logger.severe("failed to load device gallery", snapshot.error);
                 return const Text("Failed to load albums");
                 return const Text("Failed to load albums");
               } else {
               } else {
                 return const EnteLoadingWidget();
                 return const EnteLoadingWidget();

+ 17 - 7
lib/ui/collections_gallery_widget.dart

@@ -83,10 +83,18 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
   Future<List<CollectionWithThumbnail>> _getCollections() async {
   Future<List<CollectionWithThumbnail>> _getCollections() async {
     final List<CollectionWithThumbnail> collectionsWithThumbnail =
     final List<CollectionWithThumbnail> collectionsWithThumbnail =
         await CollectionsService.instance.getCollectionsWithThumbnails();
         await CollectionsService.instance.getCollectionsWithThumbnails();
-    final result = collectionsWithThumbnail.splitMatch(
+    final ListMatch<CollectionWithThumbnail> favMathResult =
+        collectionsWithThumbnail.splitMatch(
       (element) => element.collection.type == CollectionType.favorites,
       (element) => element.collection.type == CollectionType.favorites,
     );
     );
-    result.unmatched.sort(
+    // Hide fav collection if it's empty and not shared
+    favMathResult.matched.removeWhere(
+      (element) =>
+          element.thumbnail == null &&
+          (element.collection.publicURLs?.isEmpty ?? false),
+    );
+
+    favMathResult.unmatched.sort(
       (first, second) {
       (first, second) {
         if (sortKey == AlbumSortKey.albumName) {
         if (sortKey == AlbumSortKey.albumName) {
           return compareAsciiLowerCaseNatural(
           return compareAsciiLowerCaseNatural(
@@ -102,12 +110,14 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
         }
         }
       },
       },
     );
     );
-    result.matched.removeWhere(
-      (element) =>
-          element.thumbnail == null &&
-          (element.collection.publicURLs?.isEmpty ?? false),
+    // This is a way to identify collection which were automatically created
+    // during create link flow for selected files
+    final ListMatch<CollectionWithThumbnail> potentialSharedLinkCollection =
+        favMathResult.unmatched.splitMatch(
+      (e) => (e.collection.isSharedFilesCollection()),
     );
     );
-    return result.matched + result.unmatched;
+
+    return favMathResult.matched + potentialSharedLinkCollection.unmatched;
   }
   }
 
 
   Widget _getCollectionsGalleryWidget(
   Widget _getCollectionsGalleryWidget(

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

@@ -14,7 +14,34 @@ enum ActionSheetType {
   iconOnly,
   iconOnly,
 }
 }
 
 
-Future<dynamic> showActionSheet({
+Future<ButtonAction?> showCommonActionSheet({
+  required BuildContext context,
+  required List<ButtonWidget> buttons,
+  required ActionSheetType actionSheetType,
+  bool isCheckIconGreen = false,
+  String? title,
+  String? body,
+}) {
+  return showMaterialModalBottomSheet(
+    backgroundColor: Colors.transparent,
+    barrierColor: backdropFaintDark,
+    useRootNavigator: true,
+    context: context,
+    builder: (_) {
+      return ActionSheetWidget(
+        title: title,
+        body: body,
+        actionButtons: buttons,
+        actionSheetType: actionSheetType,
+        isCheckIconGreen: isCheckIconGreen,
+      );
+    },
+    isDismissible: false,
+    enableDrag: false,
+  );
+}
+
+Future<ButtonAction?> showActionSheet({
   required BuildContext context,
   required BuildContext context,
   required List<ButtonWidget> buttons,
   required List<ButtonWidget> buttons,
   required ActionSheetType actionSheetType,
   required ActionSheetType actionSheetType,

+ 110 - 104
lib/ui/components/button_widget.dart

@@ -203,119 +203,125 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
       onTapDown: _shouldRegisterGestures ? _onTapDown : null,
       onTapDown: _shouldRegisterGestures ? _onTapDown : null,
       onTapUp: _shouldRegisterGestures ? _onTapUp : null,
       onTapUp: _shouldRegisterGestures ? _onTapUp : null,
       onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
       onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
-      child: AnimatedContainer(
-        duration: const Duration(milliseconds: 16),
-        width: widget.buttonSize == ButtonSize.large ? double.infinity : null,
+      child: Container(
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           borderRadius: const BorderRadius.all(Radius.circular(4)),
           borderRadius: const BorderRadius.all(Radius.circular(4)),
-          color: buttonColor,
           border: Border.all(color: borderColor),
           border: Border.all(color: borderColor),
         ),
         ),
-        child: Padding(
-          padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
-          child: AnimatedSwitcher(
-            duration: const Duration(milliseconds: 175),
-            switchInCurve: Curves.easeInOutExpo,
-            switchOutCurve: Curves.easeInOutExpo,
-            child: executionState == ExecutionState.idle
-                ? widget.buttonType.hasTrailingIcon
-                    ? Row(
-                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                        children: [
-                          widget.labelText == null
-                              ? const SizedBox.shrink()
-                              : Flexible(
-                                  child: Padding(
-                                    padding: widget.icon == null
-                                        ? const EdgeInsets.symmetric(
-                                            horizontal: 8,
-                                          )
-                                        : const EdgeInsets.only(right: 16),
-                                    child: Text(
-                                      widget.labelText!,
-                                      overflow: TextOverflow.ellipsis,
-                                      maxLines: 2,
-                                      style: labelStyle,
+        child: AnimatedContainer(
+          duration: const Duration(milliseconds: 16),
+          width: widget.buttonSize == ButtonSize.large ? double.infinity : null,
+          decoration: BoxDecoration(
+            borderRadius: const BorderRadius.all(Radius.circular(4)),
+            color: buttonColor,
+          ),
+          child: Padding(
+            padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
+            child: AnimatedSwitcher(
+              duration: const Duration(milliseconds: 175),
+              switchInCurve: Curves.easeInOutExpo,
+              switchOutCurve: Curves.easeInOutExpo,
+              child: executionState == ExecutionState.idle
+                  ? widget.buttonType.hasTrailingIcon
+                      ? Row(
+                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                          children: [
+                            widget.labelText == null
+                                ? const SizedBox.shrink()
+                                : Flexible(
+                                    child: Padding(
+                                      padding: widget.icon == null
+                                          ? const EdgeInsets.symmetric(
+                                              horizontal: 8,
+                                            )
+                                          : const EdgeInsets.only(right: 16),
+                                      child: Text(
+                                        widget.labelText!,
+                                        overflow: TextOverflow.ellipsis,
+                                        maxLines: 2,
+                                        style: labelStyle,
+                                      ),
                                     ),
                                     ),
                                   ),
                                   ),
-                                ),
-                          widget.icon == null
-                              ? const SizedBox.shrink()
-                              : Icon(
-                                  widget.icon,
-                                  size: 20,
-                                  color: iconColor,
-                                ),
-                        ],
-                      )
-                    : Builder(
-                        builder: (context) {
-                          SchedulerBinding.instance.addPostFrameCallback(
-                            (timeStamp) {
-                              final box =
-                                  context.findRenderObject() as RenderBox;
-                              widthOfButton = box.size.width;
-                            },
-                          );
-                          return Row(
-                            mainAxisSize: widget.buttonSize == ButtonSize.large
-                                ? MainAxisSize.max
-                                : MainAxisSize.min,
+                            widget.icon == null
+                                ? const SizedBox.shrink()
+                                : Icon(
+                                    widget.icon,
+                                    size: 20,
+                                    color: iconColor,
+                                  ),
+                          ],
+                        )
+                      : Builder(
+                          builder: (context) {
+                            SchedulerBinding.instance.addPostFrameCallback(
+                              (timeStamp) {
+                                final box =
+                                    context.findRenderObject() as RenderBox;
+                                widthOfButton = box.size.width;
+                              },
+                            );
+                            return Row(
+                              mainAxisSize:
+                                  widget.buttonSize == ButtonSize.large
+                                      ? MainAxisSize.max
+                                      : MainAxisSize.min,
+                              mainAxisAlignment: MainAxisAlignment.center,
+                              children: [
+                                widget.icon == null
+                                    ? const SizedBox.shrink()
+                                    : Icon(
+                                        widget.icon,
+                                        size: 20,
+                                        color: iconColor,
+                                      ),
+                                widget.icon == null || widget.labelText == null
+                                    ? const SizedBox.shrink()
+                                    : const SizedBox(width: 8),
+                                widget.labelText == null
+                                    ? const SizedBox.shrink()
+                                    : Flexible(
+                                        child: Padding(
+                                          padding: const EdgeInsets.symmetric(
+                                            horizontal: 8,
+                                          ),
+                                          child: Text(
+                                            widget.labelText!,
+                                            style: labelStyle,
+                                            maxLines: 2,
+                                            overflow: TextOverflow.ellipsis,
+                                          ),
+                                        ),
+                                      )
+                              ],
+                            );
+                          },
+                        )
+                  : executionState == ExecutionState.inProgress
+                      ? SizedBox(
+                          width: widthOfButton,
+                          child: Row(
                             mainAxisAlignment: MainAxisAlignment.center,
                             mainAxisAlignment: MainAxisAlignment.center,
+                            mainAxisSize: MainAxisSize.min,
                             children: [
                             children: [
-                              widget.icon == null
-                                  ? const SizedBox.shrink()
-                                  : Icon(
-                                      widget.icon,
-                                      size: 20,
-                                      color: iconColor,
-                                    ),
-                              widget.icon == null || widget.labelText == null
-                                  ? const SizedBox.shrink()
-                                  : const SizedBox(width: 8),
-                              widget.labelText == null
-                                  ? const SizedBox.shrink()
-                                  : Flexible(
-                                      child: Padding(
-                                        padding: const EdgeInsets.symmetric(
-                                          horizontal: 8,
-                                        ),
-                                        child: Text(
-                                          widget.labelText!,
-                                          style: labelStyle,
-                                          maxLines: 2,
-                                          overflow: TextOverflow.ellipsis,
-                                        ),
-                                      ),
-                                    )
+                              EnteLoadingWidget(
+                                is20pts: true,
+                                color: loadingIconColor,
+                              ),
                             ],
                             ],
-                          );
-                        },
-                      )
-                : executionState == ExecutionState.inProgress
-                    ? SizedBox(
-                        width: widthOfButton,
-                        child: Row(
-                          mainAxisAlignment: MainAxisAlignment.center,
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            EnteLoadingWidget(
-                              is20pts: true,
-                              color: loadingIconColor,
-                            ),
-                          ],
-                        ),
-                      )
-                    : executionState == ExecutionState.successful
-                        ? SizedBox(
-                            width: widthOfButton,
-                            child: Icon(
-                              Icons.check_outlined,
-                              size: 20,
-                              color: checkIconColor,
-                            ),
-                          )
-                        : const SizedBox.shrink(), //fallback
+                          ),
+                        )
+                      : executionState == ExecutionState.successful
+                          ? SizedBox(
+                              width: widthOfButton,
+                              child: Icon(
+                                Icons.check_outlined,
+                                size: 20,
+                                color: checkIconColor,
+                              ),
+                            )
+                          : const SizedBox.shrink(), //fallback
+            ),
           ),
           ),
         ),
         ),
       ),
       ),

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

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

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

@@ -196,7 +196,12 @@ class Actions extends StatelessWidget {
       children: addSeparators(
       children: addSeparators(
         buttons,
         buttons,
         const SizedBox(
         const SizedBox(
-          height: 8,
+          // In figma this white space is of height 8pts. But the Button
+          // component has 1pts of invisible border by default in code. So two
+          // 1pts borders will visually make the whitespace 8pts.
+          // Height of button component in figma = 48, in code = 50 (2pts for
+          // top + bottom border)
+          height: 6,
         ),
         ),
       ),
       ),
     );
     );

+ 5 - 0
lib/ui/create_collection_page.dart

@@ -203,6 +203,11 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
       includeCollabCollections:
       includeCollabCollections:
           widget.actionType == CollectionActionType.addFiles,
           widget.actionType == CollectionActionType.addFiles,
     );
     );
+    collectionsWithThumbnail.removeWhere(
+      (element) => (element.collection.type == CollectionType.favorites ||
+          element.collection.type == CollectionType.uncategorized ||
+          element.collection.isSharedFilesCollection()),
+    );
     collectionsWithThumbnail.sort((first, second) {
     collectionsWithThumbnail.sort((first, second) {
       return compareAsciiLowerCaseNatural(
       return compareAsciiLowerCaseNatural(
         first.collection.name ?? "",
         first.collection.name ?? "",

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

@@ -381,7 +381,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
           : await _billingService.cancelStripeSubscription();
           : await _billingService.cancelStripeSubscription();
       await _fetchSub();
       await _fetchSub();
     } catch (e) {
     } catch (e) {
-      showToast(
+      showShortToast(
         context,
         context,
         isRenewCancelled ? 'failed to renew' : 'failed to cancel',
         isRenewCancelled ? 'failed to renew' : 'failed to cancel',
       );
       );

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

@@ -86,7 +86,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
                 text = "Your plan was successfully downgraded";
                 text = "Your plan was successfully downgraded";
               }
               }
             }
             }
-            showToast(context, text);
+            showShortToast(context, text);
             _currentSubscription = newSubscription;
             _currentSubscription = newSubscription;
             _hasActiveSubscription = _currentSubscription.isValid();
             _hasActiveSubscription = _currentSubscription.isValid();
             setState(() {});
             setState(() {});

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

@@ -79,7 +79,8 @@ class AboutSectionWidget extends StatelessWidget {
                           barrierColor: Colors.black.withOpacity(0.85),
                           barrierColor: Colors.black.withOpacity(0.85),
                         );
                         );
                       } else {
                       } else {
-                        showToast(context, "You are on the latest version");
+                        showShortToast(
+                            context, "You are on the latest version");
                       }
                       }
                     },
                     },
                   ),
                   ),

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

@@ -241,7 +241,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
             "Ok",
             "Ok",
           ),
           ),
           onPressed: () {
           onPressed: () {
-            showToast(
+            showShortToast(
               context,
               context,
               "Also empty your \"Trash\" to claim the freed up space",
               "Also empty your \"Trash\" to claim the freed up space",
             );
             );

+ 2 - 2
lib/ui/settings/debug_section_widget.dart

@@ -52,7 +52,7 @@ class DebugSectionWidget extends StatelessWidget {
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
             await LocalSyncService.instance.resetLocalSync();
             await LocalSyncService.instance.resetLocalSync();
-            showToast(context, "Done");
+            showShortToast(context, "Done");
           },
           },
         ),
         ),
         sectionOptionSpacing,
         sectionOptionSpacing,
@@ -66,7 +66,7 @@ class DebugSectionWidget extends StatelessWidget {
           onTap: () async {
           onTap: () async {
             await IgnoredFilesService.instance.reset();
             await IgnoredFilesService.instance.reset();
             SyncService.instance.sync();
             SyncService.instance.sync();
-            showToast(context, "Done");
+            showShortToast(context, "Done");
           },
           },
         ),
         ),
         sectionOptionSpacing,
         sectionOptionSpacing,

+ 1 - 3
lib/ui/settings_page.dart

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/opened_settings_event.dart';
 import 'package:photos/events/opened_settings_event.dart';
-import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/settings/about_section_widget.dart';
 import 'package:photos/ui/settings/about_section_widget.dart';
@@ -99,8 +98,7 @@ class SettingsPage extends StatelessWidget {
       const AboutSectionWidget(),
       const AboutSectionWidget(),
     ]);
     ]);
 
 
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
-        hasLoggedIn) {
+    if (hasLoggedIn) {
       contents.addAll([sectionSpacing, const DebugSectionWidget()]);
       contents.addAll([sectionSpacing, const DebugSectionWidget()]);
     }
     }
     contents.add(const AppVersionWidget());
     contents.add(const AppVersionWidget());

+ 13 - 3
lib/ui/shared_collections_gallery.dart

@@ -73,7 +73,9 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
           final c =
           final c =
               CollectionsService.instance.getCollectionByID(file.collectionID);
               CollectionsService.instance.getCollectionByID(file.collectionID);
           if (c.owner.id == Configuration.instance.getUserID()) {
           if (c.owner.id == Configuration.instance.getUserID()) {
-            if (c.sharees.isNotEmpty || c.publicURLs.isNotEmpty) {
+            if (c.sharees.isNotEmpty ||
+                c.publicURLs.isNotEmpty ||
+                c.isSharedFilesCollection()) {
               outgoing.add(
               outgoing.add(
                 CollectionWithThumbnail(
                 CollectionWithThumbnail(
                   c,
                   c,
@@ -91,8 +93,16 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
           }
           }
         }
         }
         outgoing.sort((first, second) {
         outgoing.sort((first, second) {
-          return second.collection.updationTime
-              .compareTo(first.collection.updationTime);
+          if (second.collection.isSharedFilesCollection() ==
+              first.collection.isSharedFilesCollection()) {
+            return second.collection.updationTime
+                .compareTo(first.collection.updationTime);
+          } else {
+            if (first.collection.isSharedFilesCollection()) {
+              return 1;
+            }
+            return -1;
+          }
         });
         });
         incoming.sort((first, second) {
         incoming.sort((first, second) {
           return second.collection.updationTime
           return second.collection.updationTime

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

@@ -492,7 +492,7 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
     try {
     try {
       await CollectionsService.instance.updateShareUrl(widget.collection, prop);
       await CollectionsService.instance.updateShareUrl(widget.collection, prop);
       await dialog.hide();
       await dialog.hide();
-      showToast(context, "Album updated");
+      showShortToast(context, "Album updated");
     } catch (e) {
     } catch (e) {
       await dialog.hide();
       await dialog.hide();
       await showGenericErrorDialog(context: context);
       await showGenericErrorDialog(context: context);

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

@@ -145,7 +145,7 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
               pressedColor: getEnteColorScheme(context).fillFaint,
               pressedColor: getEnteColorScheme(context).fillFaint,
               onTap: () async {
               onTap: () async {
                 await Clipboard.setData(ClipboardData(text: url));
                 await Clipboard.setData(ClipboardData(text: url));
-                showToast(context, "Link copied to clipboard");
+                showShortToast(context, "Link copied to clipboard");
               },
               },
               isBottomBorderRadiusRemoved: true,
               isBottomBorderRadiusRemoved: true,
             ),
             ),

+ 192 - 0
lib/ui/tools/debug/app_storage_viewer.dart

@@ -0,0 +1,192 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:path_provider/path_provider.dart';
+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/captioned_text_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/ui/components/menu_section_title.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/ui/tools/debug/path_storage_viewer.dart';
+import 'package:photos/utils/directory_content.dart';
+
+class AppStorageViewer extends StatefulWidget {
+  const AppStorageViewer({Key? key}) : super(key: key);
+
+  @override
+  State<AppStorageViewer> createState() => _AppStorageViewerState();
+}
+
+class _AppStorageViewerState extends State<AppStorageViewer> {
+  final List<PathStorageItem> paths = [];
+  late bool internalUser;
+  int _refreshCounterKey = 0;
+
+  @override
+  void initState() {
+    internalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
+    addPath();
+    super.initState();
+  }
+
+  void addPath() async {
+    final appDocumentsDirectory = (await getApplicationDocumentsDirectory());
+    final appSupportDirectory = (await getApplicationSupportDirectory());
+    final appTemporaryDirectory = (await getTemporaryDirectory());
+    final iOSOnlyTempDirectory = "${appDocumentsDirectory.parent.path}/tmp/";
+    final iOSPhotoManagerInAppCacheDirectory =
+        iOSOnlyTempDirectory + "flutter-images";
+    final androidGlideCacheDirectory =
+        "${appTemporaryDirectory.path}/image_manager_disk_cache/";
+
+    final String tempDownload = Configuration.instance.getTempDirectory();
+    final String cacheDirectory =
+        Configuration.instance.getThumbnailCacheDirectory();
+    final imageCachePath =
+        appTemporaryDirectory.path + "/" + DefaultCacheManager.key;
+    final videoCachePath =
+        appTemporaryDirectory.path + "/" + VideoCacheManager.key;
+    paths.addAll([
+      PathStorageItem.name(
+        imageCachePath,
+        "Remote images",
+        allowCacheClear: true,
+      ),
+      PathStorageItem.name(
+        videoCachePath,
+        "Remote videos",
+        allowCacheClear: true,
+      ),
+      PathStorageItem.name(
+        cacheDirectory,
+        "Remote thumbnails",
+        allowCacheClear: true,
+      ),
+      PathStorageItem.name(tempDownload, "Pending sync"),
+      PathStorageItem.name(
+        Platform.isAndroid
+            ? androidGlideCacheDirectory
+            : iOSPhotoManagerInAppCacheDirectory,
+        "Local gallery",
+        allowCacheClear: true,
+      ),
+    ]);
+    if (internalUser) {
+      paths.addAll([
+        PathStorageItem.name(appDocumentsDirectory.path, "App Documents Dir"),
+        PathStorageItem.name(appSupportDirectory.path, "App Support Dir"),
+        PathStorageItem.name(appTemporaryDirectory.path, "App Temp Dir"),
+      ]);
+    }
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    debugPrint("$runtimeType building");
+
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          TitleBarWidget(
+            flexibleSpaceTitle: const TitleBarTitleWidget(
+              title: "Manage device storage",
+            ),
+            actionIcons: [
+              IconButtonWidget(
+                icon: Icons.close_outlined,
+                iconButtonType: IconButtonType.secondary,
+                onTap: () {
+                  Navigator.pop(context);
+                  if (Navigator.canPop(context)) {
+                    Navigator.pop(context);
+                  }
+                  if (Navigator.canPop(context)) {
+                    Navigator.pop(context);
+                  }
+                },
+              ),
+            ],
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 16),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Column(
+                        children: [
+                          const MenuSectionTitle(
+                            title: 'Cached data',
+                          ),
+                          ListView.builder(
+                            shrinkWrap: true,
+                            padding: const EdgeInsets.all(0),
+                            physics: const ScrollPhysics(),
+                            // to disable GridView's scrolling
+                            itemBuilder: (context, index) {
+                              final path = paths[index];
+                              return PathStorageViewer(
+                                path,
+                                removeTopRadius: index > 0,
+                                removeBottomRadius: index < paths.length - 1,
+                                enableDoubleTapClear: internalUser,
+                                key: ValueKey("$index-$_refreshCounterKey"),
+                              );
+                            },
+                            itemCount: paths.length,
+                          ),
+                          const SizedBox(
+                            height: 24,
+                          ),
+                          MenuItemWidget(
+                            leadingIcon: Icons.delete_sweep_outlined,
+                            captionedTextWidget: const CaptionedTextWidget(
+                              title: "Clear caches",
+                            ),
+                            menuItemColor:
+                                getEnteColorScheme(context).fillFaint,
+                            borderRadius: 8,
+                            onTap: () async {
+                              for (var pathItem in paths) {
+                                if (pathItem.allowCacheClear) {
+                                  await deleteDirectoryContents(
+                                    pathItem.path,
+                                  );
+                                }
+                              }
+                              _refreshCounterKey++;
+                              if (mounted) {
+                                setState(() => {});
+                              }
+                            },
+                          ),
+                          const SizedBox(
+                            height: 24,
+                          ),
+                        ],
+                      ),
+                    ],
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 119 - 0
lib/ui/tools/debug/path_storage_viewer.dart

@@ -0,0 +1,119 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/utils/data_util.dart';
+import 'package:photos/utils/directory_content.dart';
+
+class PathStorageItem {
+  final String path;
+  final String title;
+  final bool allowCacheClear;
+
+  PathStorageItem.name(
+    this.path,
+    this.title, {
+    this.allowCacheClear = false,
+  });
+}
+
+class PathStorageViewer extends StatefulWidget {
+  final PathStorageItem item;
+  final bool removeTopRadius;
+  final bool removeBottomRadius;
+  final bool enableDoubleTapClear;
+
+  const PathStorageViewer(
+    this.item, {
+    this.removeTopRadius = false,
+    this.removeBottomRadius = false,
+    this.enableDoubleTapClear = false,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<PathStorageViewer> createState() => _PathStorageViewerState();
+}
+
+class _PathStorageViewerState extends State<PathStorageViewer> {
+  final Logger _logger = Logger((_PathStorageViewerState).toString());
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  void _safeRefresh() async {
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder<DirectoryStat>(
+      future: getDirectorySize(Directory(widget.item.path)),
+      builder: (context, snapshot) {
+        if (snapshot.hasData) {
+          return _buildMenuItemWidget(snapshot.data, null);
+        } else if (snapshot.hasError) {
+          _logger.severe(
+            "Failed to get state for ${widget.item.title}",
+            snapshot.error,
+          );
+          return _buildMenuItemWidget(null, snapshot.error);
+        } else {
+          return _buildMenuItemWidget(null, null);
+        }
+      },
+    );
+  }
+
+  Widget _buildMenuItemWidget(DirectoryStat? stat, Object? err) {
+    return MenuItemWidget(
+      alignCaptionedTextToLeft: true,
+      captionedTextWidget: CaptionedTextWidget(
+        title: widget.item.title,
+        subTitle: stat != null ? '${stat.fileCount}' : null,
+        subTitleColor: getEnteColorScheme(context).textFaint,
+      ),
+      trailingWidget: stat != null
+          ? Text(
+              formatBytes(stat.size),
+              style: getEnteTextTheme(context)
+                  .small
+                  .copyWith(color: getEnteColorScheme(context).textFaint),
+            )
+          : SizedBox.fromSize(
+              size: const Size.square(14),
+              child: CircularProgressIndicator(
+                strokeWidth: 2,
+                color: getEnteColorScheme(context).strokeMuted,
+              ),
+            ),
+      trailingIcon: err != null ? Icons.error_outline_outlined : null,
+      trailingIconIsMuted: err != null,
+      borderRadius: 8,
+      menuItemColor: getEnteColorScheme(context).fillFaint,
+      isBottomBorderRadiusRemoved: widget.removeBottomRadius,
+      isTopBorderRadiusRemoved: widget.removeTopRadius,
+      onTap: () async {
+        if (kDebugMode) {
+          await Clipboard.setData(ClipboardData(text: widget.item.path));
+          debugPrint(widget.item.path);
+        }
+      },
+      onDoubleTap: () async {
+        if (widget.item.allowCacheClear && widget.enableDoubleTapClear) {
+          await deleteDirectoryContents(widget.item.path);
+          _safeRefresh();
+        }
+      },
+    );
+  }
+}

+ 26 - 23
lib/ui/tools/editor/image_editor_page.dart

@@ -18,6 +18,9 @@ import 'package:photos/models/location.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/ui/common/loading_widget.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/models/button_type.dart';
 import 'package:photos/ui/tools/editor/filtered_image.dart';
 import 'package:photos/ui/tools/editor/filtered_image.dart';
 import 'package:photos/ui/viewer/file/detail_page.dart';
 import 'package:photos/ui/viewer/file/detail_page.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -362,7 +365,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
       await LocalSyncService.instance.trackEditedFile(newFile);
       await LocalSyncService.instance.trackEditedFile(newFile);
       Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
       Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
       SyncService.instance.sync();
       SyncService.instance.sync();
-      showToast(context, "Edits saved");
+      showShortToast(context, "Edits saved");
       _logger.info("Original file " + widget.originalFile.toString());
       _logger.info("Original file " + widget.originalFile.toString());
       _logger.info("Saved edits to file " + newFile.toString());
       _logger.info("Saved edits to file " + newFile.toString());
       final existingFiles = widget.detailPageConfig.files;
       final existingFiles = widget.detailPageConfig.files;
@@ -495,31 +498,31 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
   }
   }
 
 
   Future<void> _showExitConfirmationDialog() async {
   Future<void> _showExitConfirmationDialog() async {
-    final AlertDialog alert = AlertDialog(
-      title: const Text("Discard edits?"),
-      actions: [
-        TextButton(
-          child: const Text("Yes", style: TextStyle(color: Colors.red)),
-          onPressed: () {
-            Navigator.of(context, rootNavigator: true).pop('dialog');
-            replacePage(context, DetailPage(widget.detailPageConfig));
-          },
+    final actionResult = await showActionSheet(
+      context: context,
+      buttons: [
+        const ButtonWidget(
+          labelText: "Yes, discard changes",
+          buttonType: ButtonType.critical,
+          buttonSize: ButtonSize.large,
+          shouldStickToDarkTheme: true,
+          buttonAction: ButtonAction.first,
+          isInAlert: true,
         ),
         ),
-        TextButton(
-          child: const Text("No", style: TextStyle(color: Colors.white)),
-          onPressed: () {
-            Navigator.of(context, rootNavigator: true).pop('dialog');
-          },
+        const ButtonWidget(
+          labelText: "No",
+          buttonType: ButtonType.secondary,
+          buttonSize: ButtonSize.large,
+          buttonAction: ButtonAction.second,
+          shouldStickToDarkTheme: true,
+          isInAlert: true,
         ),
         ),
       ],
       ],
+      body: "Do you want to discard the edits you have made?",
+      actionSheetType: ActionSheetType.defaultActionSheet,
     );
     );
-
-    await showDialog(
-      context: context,
-      builder: (BuildContext context) {
-        return alert;
-      },
-      barrierColor: Colors.black87,
-    );
+    if (actionResult != null && actionResult == ButtonAction.first) {
+      replacePage(context, DetailPage(widget.detailPageConfig));
+    }
   }
   }
 }
 }

+ 121 - 12
lib/ui/viewer/actions/file_selection_actions_widget.dart

@@ -1,5 +1,7 @@
+import 'package:fast_base58/fast_base58.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection.dart';
@@ -13,11 +15,17 @@ import 'package:photos/services/hidden_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/actions/collection/collection_file_actions.dart';
 import 'package:photos/ui/actions/collection/collection_file_actions.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.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/blur_menu_item_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/bottom_action_bar/expanded_menu_widget.dart';
+import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/create_collection_page.dart';
+import 'package:photos/ui/sharing/manage_links_widget.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/magic_util.dart';
+import 'package:photos/utils/navigation_util.dart';
+import 'package:photos/utils/toast_util.dart';
 
 
 class FileSelectionActionWidget extends StatefulWidget {
 class FileSelectionActionWidget extends StatefulWidget {
   final GalleryType type;
   final GalleryType type;
@@ -43,6 +51,11 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
   late SelectedFileSplit split;
   late SelectedFileSplit split;
   late CollectionActions collectionActions;
   late CollectionActions collectionActions;
 
 
+  // _cachedCollectionForSharedLink is primarly used to avoid creating duplicate
+  // links if user keeps on creating Create link button after selecting
+  // few files. This link is reset on any selection changed;
+  Collection? _cachedCollectionForSharedLink;
+
   @override
   @override
   void initState() {
   void initState() {
     currentUserID = Configuration.instance.getUserID()!;
     currentUserID = Configuration.instance.getUserID()!;
@@ -59,6 +72,9 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
   }
   }
 
 
   void _selectFileChangeListener() {
   void _selectFileChangeListener() {
+    if (_cachedCollectionForSharedLink != null) {
+      _cachedCollectionForSharedLink = null;
+    }
     split = widget.selectedFiles.split(currentUserID);
     split = widget.selectedFiles.split(currentUserID);
     if (mounted) {
     if (mounted) {
       setState(() => {});
       setState(() => {});
@@ -88,10 +104,34 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     final colorScheme = getEnteColorScheme(context);
     final colorScheme = getEnteColorScheme(context);
     final List<List<BlurMenuItemWidget>> items = [];
     final List<List<BlurMenuItemWidget>> items = [];
     final List<BlurMenuItemWidget> firstList = [];
     final List<BlurMenuItemWidget> firstList = [];
+    final List<BlurMenuItemWidget> secondList = [];
+
+    if (widget.type.showCreateLink()) {
+      if (_cachedCollectionForSharedLink != null && anyUploadedFiles) {
+        firstList.add(
+          BlurMenuItemWidget(
+            leadingIcon: Icons.copy_outlined,
+            labelText: "Copy link",
+            menuItemColor: colorScheme.fillFaint,
+            onTap: anyUploadedFiles ? _copyLink : null,
+          ),
+        );
+      } else {
+        firstList.add(
+          BlurMenuItemWidget(
+            leadingIcon: Icons.link_outlined,
+            labelText: "Create link$suffix",
+            menuItemColor: colorScheme.fillFaint,
+            onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null,
+          ),
+        );
+      }
+    }
+
     final showUploadIcon = widget.type == GalleryType.localFolder &&
     final showUploadIcon = widget.type == GalleryType.localFolder &&
         split.ownedByCurrentUser.isEmpty;
         split.ownedByCurrentUser.isEmpty;
     if (widget.type.showAddToAlbum()) {
     if (widget.type.showAddToAlbum()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon:
           leadingIcon:
               showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
               showUploadIcon ? Icons.cloud_upload_outlined : Icons.add_outlined,
@@ -103,7 +143,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
       );
       );
     }
     }
     if (widget.type.showMoveToAlbum()) {
     if (widget.type.showMoveToAlbum()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.arrow_forward_outlined,
           leadingIcon: Icons.arrow_forward_outlined,
           labelText: "Move to album$suffix",
           labelText: "Move to album$suffix",
@@ -114,7 +154,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     }
     }
 
 
     if (showRemoveOption) {
     if (showRemoveOption) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.remove_outlined,
           leadingIcon: Icons.remove_outlined,
           labelText: "Remove from album$suffix",
           labelText: "Remove from album$suffix",
@@ -125,7 +165,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     }
     }
 
 
     if (widget.type.showDeleteOption()) {
     if (widget.type.showDeleteOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.delete_outline,
           leadingIcon: Icons.delete_outline,
           labelText: "Delete$suffixInPending",
           labelText: "Delete$suffixInPending",
@@ -136,7 +176,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     }
     }
 
 
     if (widget.type.showHideOption()) {
     if (widget.type.showHideOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.visibility_off_outlined,
           leadingIcon: Icons.visibility_off_outlined,
           labelText: "Hide$suffix",
           labelText: "Hide$suffix",
@@ -145,7 +185,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
         ),
         ),
       );
       );
     } else if (widget.type.showUnHideOption()) {
     } else if (widget.type.showUnHideOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.visibility_off_outlined,
           leadingIcon: Icons.visibility_off_outlined,
           labelText: "Unhide$suffix",
           labelText: "Unhide$suffix",
@@ -155,7 +195,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
       );
       );
     }
     }
     if (widget.type.showArchiveOption()) {
     if (widget.type.showArchiveOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.archive_outlined,
           leadingIcon: Icons.archive_outlined,
           labelText: "Archive$suffix",
           labelText: "Archive$suffix",
@@ -164,7 +204,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
         ),
         ),
       );
       );
     } else if (widget.type.showUnArchiveOption()) {
     } else if (widget.type.showUnArchiveOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.unarchive,
           leadingIcon: Icons.unarchive,
           labelText: "Unarchive$suffix",
           labelText: "Unarchive$suffix",
@@ -175,7 +215,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     }
     }
 
 
     if (widget.type.showFavoriteOption()) {
     if (widget.type.showFavoriteOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.favorite_border_rounded,
           leadingIcon: Icons.favorite_border_rounded,
           labelText: "Favorite$suffix",
           labelText: "Favorite$suffix",
@@ -184,7 +224,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
         ),
         ),
       );
       );
     } else if (widget.type.showUnFavoriteOption()) {
     } else if (widget.type.showUnFavoriteOption()) {
-      firstList.add(
+      secondList.add(
         BlurMenuItemWidget(
         BlurMenuItemWidget(
           leadingIcon: Icons.favorite,
           leadingIcon: Icons.favorite,
           labelText: "Remove from favorite$suffix",
           labelText: "Remove from favorite$suffix",
@@ -194,8 +234,11 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
       );
       );
     }
     }
 
 
-    if (firstList.isNotEmpty) {
-      items.add(firstList);
+    if (firstList.isNotEmpty || secondList.isNotEmpty) {
+      if (firstList.isNotEmpty) {
+        items.add(firstList);
+      }
+      items.add(secondList);
       return ExpandedMenuWidget(
       return ExpandedMenuWidget(
         items: items,
         items: items,
       );
       );
@@ -299,6 +342,72 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
     await _selectionCollectionForAction(CollectionActionType.unHide);
     await _selectionCollectionForAction(CollectionActionType.unHide);
   }
   }
 
 
+  Future<void> _onCreatedSharedLinkClicked() async {
+    if (split.ownedByCurrentUser.isEmpty) {
+      showShortToast(context, "Can only create link for files owned by you");
+      return;
+    }
+    _cachedCollectionForSharedLink ??= await collectionActions
+        .createSharedCollectionLink(context, split.ownedByCurrentUser);
+    final actionResult = await showActionSheet(
+      context: context,
+      buttons: [
+        const ButtonWidget(
+          labelText: "Copy link",
+          buttonType: ButtonType.neutral,
+          buttonSize: ButtonSize.large,
+          shouldStickToDarkTheme: true,
+          buttonAction: ButtonAction.first,
+          isInAlert: true,
+        ),
+        const ButtonWidget(
+          labelText: "Manage link",
+          buttonType: ButtonType.secondary,
+          buttonSize: ButtonSize.large,
+          buttonAction: ButtonAction.second,
+          shouldStickToDarkTheme: true,
+          isInAlert: true,
+        ),
+        const ButtonWidget(
+          labelText: "Done",
+          buttonType: ButtonType.secondary,
+          buttonSize: ButtonSize.large,
+          buttonAction: ButtonAction.third,
+          shouldStickToDarkTheme: true,
+          isInAlert: true,
+        )
+      ],
+      title: "Public link created",
+      body: "You can manage your links in the share tab.",
+      actionSheetType: ActionSheetType.defaultActionSheet,
+    );
+    if (actionResult != null && actionResult == ButtonAction.first) {
+      await _copyLink();
+    }
+    if (actionResult != null && actionResult == ButtonAction.second) {
+      routeToPage(
+        context,
+        ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink),
+      );
+    }
+    if (mounted) {
+      setState(() => {});
+    }
+  }
+
+  Future<void> _copyLink() async {
+    if (_cachedCollectionForSharedLink != null) {
+      final String collectionKey = Base58Encode(
+        CollectionsService.instance
+            .getCollectionKey(_cachedCollectionForSharedLink!.id),
+      );
+      final String url =
+          "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
+      await Clipboard.setData(ClipboardData(text: url));
+      showShortToast(context, "Link copied to clipboard");
+    }
+  }
+
   Future<Object?> _selectionCollectionForAction(
   Future<Object?> _selectionCollectionForAction(
     CollectionActionType type,
     CollectionActionType type,
   ) async {
   ) async {

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

@@ -365,7 +365,7 @@ class FadingAppBarState extends State<FadingAppBar> {
           isDestructiveAction: true,
           isDestructiveAction: true,
           onPressed: () async {
           onPressed: () async {
             await deleteFilesOnDeviceOnly(context, [file]);
             await deleteFilesOnDeviceOnly(context, [file]);
-            showToast(context, "File deleted from device");
+            showShortToast(context, "File deleted from device");
             Navigator.of(context, rootNavigator: true).pop();
             Navigator.of(context, rootNavigator: true).pop();
             // TODO: Fix behavior when inside a device folder
             // TODO: Fix behavior when inside a device folder
           },
           },

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

@@ -4,7 +4,6 @@ import 'dart:io' as io;
 
 
 import 'package:chewie/chewie.dart';
 import 'package:chewie/chewie.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:fluttertoast/fluttertoast.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
@@ -122,7 +121,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
     }
     }
     _isLoadingVideoPlayer = true;
     _isLoadingVideoPlayer = true;
     if (_file.isRemoteFile && !(await isFileCached(_file, liveVideo: true))) {
     if (_file.isRemoteFile && !(await isFileCached(_file, liveVideo: true))) {
-      showToast(context, "Downloading...", toastLength: Toast.LENGTH_LONG);
+      showShortToast(context, "Downloading...");
     }
     }
 
 
     var videoFile = await getFile(widget.file, liveVideo: true)
     var videoFile = await getFile(widget.file, liveVideo: true)

+ 29 - 3
lib/utils/date_time_util.dart

@@ -9,11 +9,11 @@ const Set<int> monthWith30Days = {4, 6, 9, 11};
 Map<int, String> _months = {
 Map<int, String> _months = {
   1: "Jan",
   1: "Jan",
   2: "Feb",
   2: "Feb",
-  3: "March",
-  4: "April",
+  3: "Mar",
+  4: "Apr",
   5: "May",
   5: "May",
   6: "Jun",
   6: "Jun",
-  7: "July",
+  7: "Jul",
   8: "Aug",
   8: "Aug",
   9: "Sep",
   9: "Sep",
   10: "Oct",
   10: "Oct",
@@ -86,6 +86,32 @@ String getDateAndMonthAndYear(DateTime dateTime) {
       dateTime.year.toString();
       dateTime.year.toString();
 }
 }
 
 
+// Create link default names:
+// Same day: "Dec 19, 2022"
+// Same month: "Dec 19 - 22, 2022"
+// Base case: "Dec 19, 2022 - Jan 7, 2023"
+String getNameForDateRange(int firstCreationTime, int secondCreationTime) {
+  final startTime = DateTime.fromMicrosecondsSinceEpoch(firstCreationTime);
+  final endTime = DateTime.fromMicrosecondsSinceEpoch(secondCreationTime);
+  // different year
+  if (startTime.year != endTime.year) {
+    return "${_months[startTime.month]!} ${startTime.day}, ${startTime.year} - "
+        "${_months[endTime.month]!} ${endTime.day}, ${endTime.year}";
+  }
+  // same year, diff month
+  if (startTime.month != endTime.month) {
+    return "${_months[startTime.month]!} ${startTime.day} - "
+        "${_months[endTime.month]!} ${endTime.day}, ${endTime.year}";
+  }
+  // same month and year, diff day
+  if (startTime.day != endTime.day) {
+    return "${_months[startTime.month]!} ${startTime.day} - "
+        "${_months[endTime.month]!} ${endTime.day}, ${endTime.year}";
+  }
+  // same day
+  return "${_months[endTime.month]!} ${endTime.day}, ${endTime.year}";
+}
+
 String getDay(DateTime dateTime) {
 String getDay(DateTime dateTime) {
   return _days[dateTime.weekday]!;
   return _days[dateTime.weekday]!;
 }
 }

+ 55 - 0
lib/utils/directory_content.dart

@@ -0,0 +1,55 @@
+import 'dart:io';
+
+class DirectoryStat {
+  final int subDirectoryCount;
+  final int size;
+  final int fileCount;
+
+  DirectoryStat(this.subDirectoryCount, this.size, this.fileCount);
+}
+
+Future<DirectoryStat> getDirectorySize(Directory directory) async {
+  int size = 0;
+  int subDirCount = 0;
+  int fileCount = 0;
+
+  if (await directory.exists()) {
+    // Get a list of all the files and directories in the directory
+    final List<FileSystemEntity> entities = directory.listSync();
+    // Iterate through the list of entities and add the sizes of the files to the total size
+    for (FileSystemEntity entity in entities) {
+      if (entity is File) {
+        size += (await File(entity.path).length());
+        fileCount++;
+      } else if (entity is Directory) {
+        subDirCount++;
+        // If the entity is a directory, recursively calculate its size
+        final DirectoryStat subDirStat =
+            await getDirectorySize(Directory(entity.path));
+        size += subDirStat.size;
+        subDirCount += subDirStat.subDirectoryCount;
+        fileCount += subDirStat.fileCount;
+      }
+    }
+  }
+  return DirectoryStat(subDirCount, size, fileCount);
+}
+
+Future<void> deleteDirectoryContents(String directoryPath) async {
+  // Mark variables as final if they don't need to be modified
+  final directory = Directory(directoryPath);
+  if (!(await directory.exists())) {
+    return;
+  }
+  final contents = await directory.list().toList();
+
+  // Iterate through the list and delete each file or directory
+  for (final fileOrDirectory in contents) {
+    await fileOrDirectory.delete();
+  }
+}
+
+Future<int> getFileSize(String path) async {
+  final file = File(path);
+  return await file.exists() ? await file.length() : 0;
+}

+ 3 - 3
lib/utils/magic_util.dart

@@ -91,7 +91,7 @@ Future<bool> editTime(
     );
     );
     return true;
     return true;
   } catch (e) {
   } catch (e) {
-    showToast(context, 'something went wrong');
+    showShortToast(context, 'something went wrong');
     return false;
     return false;
   }
   }
 }
 }
@@ -124,7 +124,7 @@ Future<bool> editFilename(
     );
     );
     return true;
     return true;
   } catch (e) {
   } catch (e) {
-    showToast(context, 'Something went wrong');
+    showShortToast(context, 'Something went wrong');
     return false;
     return false;
   }
   }
 }
 }
@@ -145,7 +145,7 @@ Future<bool> editFileCaption(
     return true;
     return true;
   } catch (e) {
   } catch (e) {
     if (context != null) {
     if (context != null) {
-      showToast(context, "Something went wrong");
+      showShortToast(context, "Something went wrong");
     }
     }
     return false;
     return false;
   }
   }