فهرست منبع

Merge pull request #720 from ente-io/create_link

Support for creating link for selected files
Neeraj Gupta 2 سال پیش
والد
کامیت
a1a7a01af9

+ 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

@@ -99,15 +99,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(
@@ -129,14 +149,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;
   }
   }
 }
 }

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

@@ -2,12 +2,17 @@ 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/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';
@@ -61,6 +66,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,

+ 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,
               ),
               ),

+ 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,

+ 5 - 0
lib/ui/create_collection_page.dart

@@ -202,6 +202,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 ?? "",

+ 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

+ 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 {

+ 26 - 0
lib/utils/date_time_util.dart

@@ -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]!;
 }
 }