diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index 3328722dbe..c4f23df419 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -110,12 +110,6 @@ class CollectionActions { BuildContext context, List files, ) async { - final dialog = createProgressDialog( - context, - S.of(context).creatingLink, - isDismissible: true, - ); - await dialog.show(); try { // create album with emptyName, use collectionCreationTime on UI to // show name @@ -143,10 +137,8 @@ class CollectionActions { await collectionsService.addOrCopyToCollection(collection.id, files); logger.finest("creating public link for the newly created album"); await CollectionsService.instance.createShareUrl(collection); - await dialog.hide(); return collection; } catch (e, s) { - await dialog.hide(); await showGenericErrorDialog(context: context, error: e); logger.severe("Failing to create link for selected files", e, s); } diff --git a/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart b/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart index 5ca6a25dcc..12a744848e 100644 --- a/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart +++ b/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import "package:flutter/material.dart"; import "package:photos/theme/ente_theme.dart"; @@ -89,11 +91,41 @@ class __BodyState extends State<_Body> { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon( - widget.icon, - size: 24, - color: getEnteColorScheme(context).textMuted, - ), + if (widget.icon == Icons.navigation_rounded) + Transform.rotate( + angle: math.pi / 2, + child: Icon( + widget.icon, + size: 24, + color: getEnteColorScheme(context).primary300, + shadows: const [ + BoxShadow( + color: Color.fromARGB(12, 0, 179, 60), + offset: Offset(0, 2.51), + blurRadius: 5.02, + spreadRadius: 0, + ), + BoxShadow( + color: Color.fromARGB(24, 0, 179, 60), + offset: Offset(0, 1.25), + blurRadius: 3.76, + spreadRadius: 0, + ), + BoxShadow( + color: Color.fromARGB(24, 0, 179, 60), + offset: Offset(0, 0.63), + blurRadius: 1.88, + spreadRadius: 0, + ), + ], + ), + ) + else + Icon( + widget.icon, + size: 24, + color: getEnteColorScheme(context).textMuted, + ), const SizedBox(height: 4), Text( widget.labelText, diff --git a/mobile/lib/ui/components/title_bar_title_widget.dart b/mobile/lib/ui/components/title_bar_title_widget.dart index a685ea0456..e838849ef3 100644 --- a/mobile/lib/ui/components/title_bar_title_widget.dart +++ b/mobile/lib/ui/components/title_bar_title_widget.dart @@ -6,11 +6,13 @@ class TitleBarTitleWidget extends StatelessWidget { final bool isTitleH2; final IconData? icon; final VoidCallback? onTap; + final String? heroTag; const TitleBarTitleWidget({ this.title, this.isTitleH2 = false, this.icon, this.onTap, + this.heroTag, super.key, }); @@ -51,7 +53,10 @@ class TitleBarTitleWidget extends StatelessWidget { maxLines: 1, ); } - return GestureDetector(onTap: onTap, child: widget); + return GestureDetector( + onTap: onTap, + child: heroTag != null ? Hero(tag: heroTag!, child: widget) : widget, + ); } return const SizedBox.shrink(); diff --git a/mobile/lib/ui/sharing/show_images_prevew.dart b/mobile/lib/ui/sharing/show_images_prevew.dart new file mode 100644 index 0000000000..eb04e49a6c --- /dev/null +++ b/mobile/lib/ui/sharing/show_images_prevew.dart @@ -0,0 +1,401 @@ +import 'dart:math' as math; +import "dart:ui"; + +import "package:figma_squircle/figma_squircle.dart"; +import "package:flutter/material.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/viewer/file/thumbnail_widget.dart"; + +class LinkPlaceholder extends StatelessWidget { + const LinkPlaceholder({ + required this.files, + super.key, + }); + + final List files; + + @override + Widget build(BuildContext context) { + final int length = files.length; + Widget placeholderWidget = const SizedBox( + height: 300, + width: 300, + ); + + if (length == 1) { + placeholderWidget = _BackDrop( + backDropImage: files[0], + children: [ + LayoutBuilder( + builder: (context, constraints) { + final imageHeight = constraints.maxHeight * 0.9; + return Center( + child: _CustomImage( + width: imageHeight, + height: imageHeight, + file: files[0], + zIndex: 0, + ), + ); + }, + ), + ], + ); + } else if (length == 2) { + placeholderWidget = _BackDrop( + backDropImage: files[0], + children: [ + LayoutBuilder( + builder: ((context, constraints) { + final imageHeight = constraints.maxHeight * 0.52; + return Stack( + children: [ + Positioned( + top: 145, + left: 180, + child: _CustomImage( + height: imageHeight, + width: imageHeight, + file: files[1], + zIndex: 10 * math.pi / 180, + ), + ), + Positioned( + top: 45, + left: 3.2, + child: _CustomImage( + height: imageHeight, + width: imageHeight, + file: files[0], + zIndex: -(10 * math.pi / 180), + imageShadow: const [ + BoxShadow( + offset: Offset(0, 0), + blurRadius: 0.84, + color: Color.fromRGBO(0, 0, 0, 0.11), + ), + BoxShadow( + offset: Offset(0.84, 0.84), + blurRadius: 1.68, + color: Color.fromRGBO(0, 0, 0, 0.09), + ), + BoxShadow( + offset: Offset(2.53, 2.53), + blurRadius: 2.53, + color: Color.fromRGBO(0, 0, 0, 0.05), + ), + BoxShadow( + offset: Offset(5.05, 4.21), + blurRadius: 2.53, + color: Color.fromRGBO(0, 0, 0, 0.02), + ), + BoxShadow( + offset: Offset(7.58, 6.74), + blurRadius: 2.53, + color: Color.fromRGBO(0, 0, 0, 0.0), + ), + ], + ), + ), + ], + ); + }), + ), + ], + ); + } else if (length == 3) { + placeholderWidget = _BackDrop( + backDropImage: files[0], + children: [ + LayoutBuilder( + builder: (context, constraint) { + final imageHeightSmall = constraint.maxHeight * 0.43; + final imageHeightLarge = constraint.maxHeight * 0.50; + return Stack( + children: [ + Positioned( + top: 55, + child: _CustomImage( + height: imageHeightSmall, + width: imageHeightSmall, + file: files[1], + zIndex: -(20 * math.pi / 180), + ), + ), + Positioned( + bottom: 50, + right: -10, + child: _CustomImage( + height: imageHeightSmall, + width: imageHeightSmall, + file: files[2], + zIndex: 20 * math.pi / 180, + ), + ), + Center( + child: _CustomImage( + height: imageHeightLarge, + width: imageHeightLarge, + file: files[0], + zIndex: 0.0, + imageShadow: const [ + BoxShadow( + offset: Offset(0, 1.02), + blurRadius: 2.04, + color: Color.fromRGBO(0, 0, 0, 0.23), + ), + BoxShadow( + offset: Offset(0, 3.06), + blurRadius: 3.06, + color: Color.fromRGBO(0, 0, 0, 0.2), + ), + BoxShadow( + offset: Offset(0, 6.12), + blurRadius: 4.08, + color: Color.fromRGBO(0, 0, 0, 0.12), + ), + BoxShadow( + offset: Offset(0, 11.22), + blurRadius: 5.1, + color: Color.fromRGBO(0, 0, 0, 0.04), + ), + BoxShadow( + offset: Offset(0, 18.36), + blurRadius: 5.1, + color: Color.fromRGBO(0, 0, 0, 0.0), + ), + ], + ), + ), + ], + ); + }, + ), + ], + ); + } else if (length > 3) { + placeholderWidget = _BackDrop( + backDropImage: files[0], + children: [ + LayoutBuilder( + builder: (context, constraint) { + final imageHeightSmall = constraint.maxHeight * 0.43; + final imageHeightLarge = constraint.maxHeight * 0.50; + final boxHeight = constraint.maxHeight * 0.15; + return Stack( + children: [ + Positioned( + top: 30, + left: 25, + child: _CustomImage( + height: imageHeightSmall, + width: imageHeightSmall, + file: files[1], + zIndex: 0.0, + ), + ), + Positioned( + top: 202, + left: 50, + child: _CustomImage( + height: imageHeightSmall, + width: imageHeightSmall, + file: files[2], + zIndex: 0.0, + ), + ), + Positioned( + top: 75, + right: 25, + child: _CustomImage( + height: imageHeightLarge, + width: imageHeightLarge, + file: files[0], + zIndex: 0.0, + imageShadow: const [ + BoxShadow( + offset: Offset(0, 1.02), + blurRadius: 2.04, + color: Color.fromRGBO(0, 0, 0, 0.23), + ), + BoxShadow( + offset: Offset(0, 3.06), + blurRadius: 3.06, + color: Color.fromRGBO(0, 0, 0, 0.2), + ), + BoxShadow( + offset: Offset(0, 6.12), + blurRadius: 4.08, + color: Color.fromRGBO(0, 0, 0, 0.12), + ), + BoxShadow( + offset: Offset(0, 11.22), + blurRadius: 5.1, + color: Color.fromRGBO(0, 0, 0, 0.04), + ), + BoxShadow( + offset: Offset(0, 18.36), + blurRadius: 5.1, + color: Color.fromRGBO(0, 0, 0, 0.0), + ), + ], + ), + ), + Positioned( + top: 290, + left: 270, + child: Stack( + children: [ + Center( + child: Container( + height: boxHeight + 1, + width: boxHeight + 1, + decoration: ShapeDecoration( + color: const Color.fromRGBO(129, 129, 129, 0.1), + shape: SmoothRectangleBorder( + borderRadius: SmoothBorderRadius( + cornerRadius: 12.5, + cornerSmoothing: 1.0, + ), + ), + ), + ), + ), + Center( + child: ClipSmoothRect( + radius: SmoothBorderRadius( + cornerRadius: 12, + cornerSmoothing: 1.0, + ), + child: Container( + height: boxHeight, + width: boxHeight, + color: const Color.fromRGBO(255, 255, 255, 1), + padding: const EdgeInsets.all(4), + child: Center( + child: FittedBox( + child: Text( + "+" "${length - 3}", + style: getEnteTextTheme(context).h3Bold, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ], + ); + } + + return placeholderWidget; + } +} + +class _BackDrop extends StatelessWidget { + const _BackDrop({ + required this.backDropImage, + required this.children, + }); + + final List children; + final EnteFile backDropImage; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: Stack( + children: [ + ThumbnailWidget( + backDropImage, + shouldShowSyncStatus: false, + shouldShowFavoriteIcon: false, + thumbnailSize: thumbnailLargeSize, + ), + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + color: Colors.transparent, + ), + ), + ...children, + ], + ), + ); + } +} + +class _CustomImage extends StatelessWidget { + const _CustomImage({ + required this.width, + required this.height, + required this.file, + required this.zIndex, + this.imageShadow, + }); + final List? imageShadow; + final EnteFile file; + final double zIndex; + final double height; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + transform: Matrix4.rotationZ(zIndex), + height: height, + width: width, + child: Stack( + children: [ + Center( + child: Container( + height: height, + width: width, + decoration: ShapeDecoration( + color: const Color.fromRGBO(129, 129, 129, 0.1), + shadows: imageShadow, + shape: SmoothRectangleBorder( + borderRadius: SmoothBorderRadius( + cornerRadius: 21.0, + cornerSmoothing: 1.0, + ), + ), + ), + ), + ), + Center( + child: SizedBox( + height: height - 2, + width: width - 2, + child: ClipSmoothRect( + radius: SmoothBorderRadius( + cornerRadius: 20.0, + cornerSmoothing: 1, + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + child: Container( + decoration: BoxDecoration(boxShadow: imageShadow), + child: ThumbnailWidget( + file, + shouldShowSyncStatus: false, + shouldShowFavoriteIcon: false, + thumbnailSize: thumbnailLargeSize, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/tabs/shared/all_quick_links_page.dart b/mobile/lib/ui/tabs/shared/all_quick_links_page.dart new file mode 100644 index 0000000000..3a42ad0b61 --- /dev/null +++ b/mobile/lib/ui/tabs/shared/all_quick_links_page.dart @@ -0,0 +1,69 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/ui/components/title_bar_title_widget.dart"; +import "package:photos/ui/tabs/shared/quick_link_album_item.dart"; + +class AllQuickLinksPage extends StatelessWidget { + final List quickLinks; + final String titleHeroTag; + const AllQuickLinksPage({ + required this.quickLinks, + required this.titleHeroTag, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 48, + leadingWidth: 48, + leading: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back_outlined, + ), + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TitleBarTitleWidget( + title: S.of(context).quickLinks, + heroTag: titleHeroTag, + ), + Text(quickLinks.length.toString()), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 16, + ), + child: ListView.separated( + itemBuilder: (context, index) { + return QuickLinkAlbumItem(c: quickLinks[index]); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 10); + }, + itemCount: quickLinks.length, + physics: const BouncingScrollPhysics(), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/tabs/shared/quick_link_album_item.dart b/mobile/lib/ui/tabs/shared/quick_link_album_item.dart index 70ed4d3dc7..9debde5e84 100644 --- a/mobile/lib/ui/tabs/shared/quick_link_album_item.dart +++ b/mobile/lib/ui/tabs/shared/quick_link_album_item.dart @@ -4,9 +4,9 @@ import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/services/collections_service.dart"; -import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; @@ -20,96 +20,123 @@ class QuickLinkAlbumItem extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); return GestureDetector( behavior: HitTestBehavior.opaque, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + border: Border.all(color: colorScheme.strokeFainter), + borderRadius: const BorderRadius.all( + Radius.circular(2), + ), + ), child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1), - child: SizedBox( - height: 60, - width: 60, - child: FutureBuilder( - future: CollectionsService.instance.getCover(c), - builder: (context, snapshot) { - if (snapshot.hasData) { - final String heroTag = heroTagPrefix + snapshot.data!.tag; - return Hero( - tag: heroTag, - child: ThumbnailWidget( - snapshot.data!, - key: ValueKey(heroTag), - ), - ); - } else { - return const NoThumbnailWidget(); - } - }, - ), - ), - ), - const Padding(padding: EdgeInsets.all(8)), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 6, + child: Row( children: [ - Text( - c.displayName, - style: getEnteTextTheme(context).body, - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), - child: FutureBuilder( - future: CollectionsService.instance.getFileCount(c), + SizedBox( + width: 60, + height: 60, + child: FutureBuilder( + future: CollectionsService.instance.getCover(c), builder: (context, snapshot) { - if (!snapshot.hasError) { - // final String textCount = NumberFormat().format(snapshot.data); - return Row( - children: [ - (!snapshot.hasData) - ? const Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: EnteLoadingWidget(size: 10), - ) - : Padding( - padding: - const EdgeInsets.only(right: 8.0), - child: Text( - S.of(context).itemCount(snapshot.data!), - style: getEnteTextTheme(context) - .smallMuted, - ), - ), - const SizedBox(width: 6), - c.hasLink - ? (c.publicURLs!.first!.isExpired - ? const Icon( - Icons.link_outlined, - color: warning500, - ) - : Icon( - Icons.link_outlined, - color: getEnteColorScheme(context) - .strokeMuted, - )) - : const SizedBox.shrink(), - ], + if (snapshot.hasData) { + final String heroTag = + heroTagPrefix + snapshot.data!.tag; + return Hero( + tag: heroTag, + child: ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(2), + ), + child: ThumbnailWidget( + snapshot.data!, + key: ValueKey(heroTag), + ), + ), ); - } else if (snapshot.hasError) { - return Text(S.of(context).somethingWentWrong); } else { - return const EnteLoadingWidget(size: 10); + return const NoThumbnailWidget(); } }, ), ), + const SizedBox(width: 12), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + c.displayName, + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 2, + ), + FutureBuilder( + future: CollectionsService.instance.getFileCount(c), + builder: (context, snapshot) { + if (!snapshot.hasError) { + if (!snapshot.hasData) { + return Row( + children: [ + EnteLoadingWidget( + size: 10, + color: colorScheme.strokeMuted, + ), + ], + ); + } + final noOfMemories = snapshot.data; + + return Row( + children: [ + Text( + noOfMemories.toString() + " \u2022 ", + style: textTheme.smallMuted, + ), + c.hasLink + ? (c.publicURLs!.first!.isExpired + ? Icon( + Icons.link_outlined, + color: colorScheme.warning500, + size: 22, + ) + : Icon( + Icons.link_outlined, + color: colorScheme.strokeMuted, + size: 22, + )) + : const SizedBox.shrink(), + ], + ); + } else if (snapshot.hasError) { + return Text(S.of(context).somethingWentWrong); + } else { + return const EnteLoadingWidget(size: 10); + } + }, + ), + ], + ), + ), + ), ], ), ), + const Flexible( + flex: 1, + child: IconButtonWidget( + icon: Icons.chevron_right_outlined, + iconButtonType: IconButtonType.secondary, + ), + ), ], ), ), diff --git a/mobile/lib/ui/tabs/shared_collections_tab.dart b/mobile/lib/ui/tabs/shared_collections_tab.dart index 450d3062ca..a0d78192ac 100644 --- a/mobile/lib/ui/tabs/shared_collections_tab.dart +++ b/mobile/lib/ui/tabs/shared_collections_tab.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import "dart:math"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -17,6 +18,7 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/components/divider_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; import 'package:photos/ui/tabs/section_title.dart'; +import "package:photos/ui/tabs/shared/all_quick_links_page.dart"; import "package:photos/ui/tabs/shared/empty_state.dart"; import "package:photos/ui/tabs/shared/quick_link_album_item.dart"; import "package:photos/utils/debouncer.dart"; @@ -97,7 +99,9 @@ class _SharedCollectionsTabState extends State Widget _getSharedCollectionsGallery(SharedCollections collections) { const maxThumbnailWidth = 160.0; - final bool hasQuickLinks = collections.quickLinks.isNotEmpty; + const maxQuickLinks = 6; + final numberOfQuickLinks = collections.quickLinks.length; + const quickLinkTitleHeroTag = "quick_link_title"; final SectionTitle sharedWithYou = SectionTitle(title: S.of(context).sharedWithYou); final SectionTitle sharedByYou = @@ -216,25 +220,56 @@ class _SharedCollectionsTabState extends State ], ), ), - hasQuickLinks + numberOfQuickLinks > 0 ? Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), child: Column( children: [ SectionOptions( - SectionTitle(title: S.of(context).quickLinks), + Hero( + tag: quickLinkTitleHeroTag, + child: SectionTitle( + title: S.of(context).quickLinks, + ), + ), + trailingWidget: numberOfQuickLinks > maxQuickLinks + ? IconButtonWidget( + icon: Icons.chevron_right, + iconButtonType: IconButtonType.secondary, + onTap: () { + unawaited( + routeToPage( + context, + AllQuickLinksPage( + titleHeroTag: quickLinkTitleHeroTag, + quickLinks: collections.quickLinks, + ), + ), + ); + }, + ) + : null, ), const SizedBox(height: 2), - ListView.builder( + ListView.separated( shrinkWrap: true, - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.only( + bottom: 12, + left: 12, + right: 12, + ), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return QuickLinkAlbumItem( c: collections.quickLinks[index], ); }, - itemCount: collections.quickLinks.length, + separatorBuilder: (context, index) { + return const SizedBox(height: 4); + }, + itemCount: min(numberOfQuickLinks, maxQuickLinks), ), ], ), @@ -248,10 +283,10 @@ class _SharedCollectionsTabState extends State Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), child: ButtonWidget( - buttonType: - !hasQuickLinks && collections.outgoing.isEmpty - ? ButtonType.trailingIconSecondary - : ButtonType.trailingIconPrimary, + buttonType: numberOfQuickLinks == 0 && + collections.outgoing.isEmpty + ? ButtonType.trailingIconSecondary + : ButtonType.trailingIconPrimary, labelText: S.of(context).inviteYourFriendsToEnte, icon: Icons.ios_share_outlined, onTap: () async { diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index beeb9164d5..d162ec9ba4 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -1,4 +1,5 @@ import "dart:async"; +import "dart:io"; import 'package:fast_base58/fast_base58.dart'; import "package:flutter/cupertino.dart"; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import "package:logging/logging.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; +import "package:path_provider/path_provider.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; @@ -32,7 +34,8 @@ import 'package:photos/ui/components/action_sheet_widget.dart'; import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart"; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; -import 'package:photos/ui/sharing/manage_links_widget.dart'; +// import 'package:photos/ui/sharing/manage_links_widget.dart'; +import "package:photos/ui/sharing/show_images_prevew.dart"; import "package:photos/ui/tools/collage/collage_creator_page.dart"; import "package:photos/ui/viewer/location/update_location_data_widget.dart"; import 'package:photos/utils/delete_file_util.dart'; @@ -42,6 +45,7 @@ import 'package:photos/utils/magic_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/share_util.dart"; import 'package:photos/utils/toast_util.dart'; +import "package:screenshot/screenshot.dart"; class FileSelectionActionsWidget extends StatefulWidget { final GalleryType type; @@ -73,12 +77,14 @@ class _FileSelectionActionsWidgetState late FilesSplit split; late CollectionActions collectionActions; late bool isCollectionOwner; - + final ScreenshotController screenshotController = ScreenshotController(); + late String? placeholderPath; // _cachedCollectionForSharedLink is primarily 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; final GlobalKey shareButtonKey = GlobalKey(); + final GlobalKey sendLinkButtonKey = GlobalKey(); @override void initState() { @@ -157,16 +163,17 @@ class _FileSelectionActionsWidgetState SelectionActionButton( icon: Icons.copy_outlined, labelText: S.of(context).copyLink, - onTap: anyUploadedFiles ? _copyLink : null, + onTap: anyUploadedFiles ? _sendLink : null, ), ); } else { items.add( SelectionActionButton( - icon: Icons.link_outlined, - labelText: S.of(context).shareLink, - onTap: anyUploadedFiles ? _onCreatedSharedLinkClicked : null, + icon: Icons.navigation_rounded, + labelText: S.of(context).sendLink, + onTap: anyUploadedFiles ? _onSendLinkTapped : null, shouldShow: ownedFilesCount > 0, + key: sendLinkButtonKey, ), ); } @@ -409,6 +416,7 @@ class _FileSelectionActionsWidgetState SelectionActionButton( labelText: S.of(context).share, icon: Icons.adaptive.share_outlined, + key: shareButtonKey, onTap: () => shareSelected( context, shareButtonKey, @@ -602,7 +610,43 @@ class _FileSelectionActionsWidgetState } } - Future _onCreatedSharedLinkClicked() async { + Future saveImage(Uint8List bytes) async { + String path = ""; + try { + final Directory root = await getTemporaryDirectory(); + final String directoryPath = '${root.path}/enteTempFiles'; + final DateTime timeStamp = DateTime.now(); + await Directory(directoryPath).create(recursive: true); + final String filePath = '$directoryPath/$timeStamp.jpg'; + final file = await File(filePath).writeAsBytes(bytes); + path = file.path; + } catch (e) { + _logger.severe("Failed to save placeholder image", e); + } + return path; + } + + Future _createPlaceholder( + List ownedSelectedFiles, + ) async { + final Widget imageWidget = LinkPlaceholder( + files: ownedSelectedFiles, + ); + await Future.delayed(const Duration(milliseconds: 100)); + final double pixelRatio = MediaQuery.of(context).devicePixelRatio; + final bytesOfImageToWidget = await screenshotController.captureFromWidget( + imageWidget, + pixelRatio: pixelRatio, + targetSize: MediaQuery.sizeOf(context), + delay: const Duration(milliseconds: 100), + ); + + final String onCreatedPlaceholderPath = + await saveImage(bytesOfImageToWidget); + return onCreatedPlaceholderPath; + } + + Future _onSendLinkTapped() async { if (split.ownedByCurrentUser.isEmpty) { showShortToast( context, @@ -610,51 +654,19 @@ class _FileSelectionActionsWidgetState ); return; } + final dialog = createProgressDialog( + context, + S.of(context).creatingLink, + isDismissible: true, + ); + await dialog.show(); _cachedCollectionForSharedLink ??= await collectionActions .createSharedCollectionLink(context, split.ownedByCurrentUser); - final actionResult = await showActionSheet( - context: context, - buttons: [ - ButtonWidget( - labelText: S.of(context).copyLink, - buttonType: ButtonType.neutral, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.first, - isInAlert: true, - ), - ButtonWidget( - labelText: S.of(context).manageLink, - buttonType: ButtonType.secondary, - buttonSize: ButtonSize.large, - buttonAction: ButtonAction.second, - shouldStickToDarkTheme: true, - isInAlert: true, - ), - ButtonWidget( - labelText: S.of(context).done, - buttonType: ButtonType.secondary, - buttonSize: ButtonSize.large, - buttonAction: ButtonAction.third, - shouldStickToDarkTheme: true, - isInAlert: true, - ), - ], - title: S.of(context).publicLinkCreated, - body: S.of(context).youCanManageYourLinksInTheShareTab, - actionSheetType: ActionSheetType.defaultActionSheet, - ); - if (actionResult?.action != null) { - if (actionResult!.action == ButtonAction.first) { - await _copyLink(); - } - if (actionResult.action == ButtonAction.second) { - await routeToPage( - context, - ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink), - ); - } - } + + final List ownedSelectedFiles = split.ownedByCurrentUser; + placeholderPath = await _createPlaceholder(ownedSelectedFiles); + await dialog.hide(); + await _sendLink(); widget.selectedFiles.clearAll(); if (mounted) { setState(() => {}); @@ -756,7 +768,7 @@ class _FileSelectionActionsWidgetState } } - Future _copyLink() async { + Future _sendLink() async { if (_cachedCollectionForSharedLink != null) { final String collectionKey = Base58Encode( CollectionsService.instance @@ -764,8 +776,25 @@ class _FileSelectionActionsWidgetState ); final String url = "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey"; - await Clipboard.setData(ClipboardData(text: url)); - showShortToast(context, S.of(context).linkCopiedToClipboard); + unawaited(Clipboard.setData(ClipboardData(text: url))); + await shareImageAndUrl( + placeholderPath!, + url, + context: context, + key: sendLinkButtonKey, + ); + if (placeholderPath != null) { + final file = File(placeholderPath!); + try { + if (file.existsSync()) { + file.deleteSync(); + } + } catch (e) { + _logger.warning("Failed to delete the file: $e"); + } finally { + placeholderPath = null; + } + } } } diff --git a/mobile/lib/utils/share_util.dart b/mobile/lib/utils/share_util.dart index ff9f691bd6..b55730439a 100644 --- a/mobile/lib/utils/share_util.dart +++ b/mobile/lib/utils/share_util.dart @@ -18,19 +18,8 @@ import 'package:share_plus/share_plus.dart'; import "package:uuid/uuid.dart"; final _logger = Logger("ShareUtil"); -// Set of possible image extensions -final _imageExtension = {"jpg", "jpeg", "png", "heic", "heif", "webp", ".gif"}; -final _videoExtension = { - "mp4", - "mov", - "avi", - "mkv", - "webm", - "wmv", - "flv", - "3gp", -}; -// share is used to share media/files from ente to other apps + +/// share is used to share media/files from ente to other apps Future share( BuildContext context, List files, { @@ -62,9 +51,13 @@ Future share( final paths = await Future.wait(pathFutures); await dialog.hide(); paths.removeWhere((element) => element == null); - final List nonNullPaths = paths.map((element) => element!).toList(); - return Share.shareFiles( - nonNullPaths, + final xFiles = []; + for (String? path in paths) { + if (path == null) continue; + xFiles.add(XFile(path)); + } + await Share.shareXFiles( + xFiles, // required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383 sharePositionOrigin: shareButtonRect(context, shareButtonKey), ); @@ -79,8 +72,10 @@ Future share( } } +/// Returns the rect of button if context and key are not null +/// If key is null, returned rect will be at the center of the screen Rect shareButtonRect(BuildContext context, GlobalKey? shareButtonKey) { - Size size = MediaQuery.of(context).size; + Size size = MediaQuery.sizeOf(context); final RenderObject? renderObject = shareButtonKey?.currentContext?.findRenderObject(); RenderBox? renderBox; @@ -99,8 +94,21 @@ Rect shareButtonRect(BuildContext context, GlobalKey? shareButtonKey) { ); } -Future shareText(String text) async { - return Share.share(text); +Future shareText( + String text, { + BuildContext? context, + GlobalKey? key, +}) async { + try { + final sharePosOrigin = _sharePosOrigin(context, key); + return Share.share( + text, + sharePositionOrigin: sharePosOrigin, + ); + } catch (e, s) { + _logger.severe("failed to share text", e, s); + return ShareResult.unavailable; + } } Future> convertIncomingSharedMediaToFile( @@ -218,3 +226,32 @@ void shareSelected( shareButtonKey: shareButtonKey, ); } + +Future shareImageAndUrl( + String imagePath, + String url, { + BuildContext? context, + GlobalKey? key, +}) async { + final sharePosOrigin = _sharePosOrigin(context, key); + + await Share.shareXFiles( + [XFile(imagePath)], + text: url, + sharePositionOrigin: sharePosOrigin, + ); +} + +/// required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383 +/// This returns the position of the share button if context and key are not null +/// and if not, it returns a default position so that the share sheet on iPad has +/// some position to show up. +Rect _sharePosOrigin(BuildContext? context, GlobalKey? key) { + late final Rect rect; + if (context != null) { + rect = shareButtonRect(context, key); + } else { + rect = const Offset(20.0, 20.0) & const Size(10, 10); + } + return rect; +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 97f5780063..e2408583c4 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1871,6 +1871,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + screenshot: + dependency: "direct main" + description: + name: screenshot + sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" + url: "https://pub.dev" + source: hosted + version: "3.0.0" scrollable_positioned_list: dependency: "direct main" description: @@ -1899,18 +1907,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "9.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "4.0.0" shared_preferences: dependency: "direct main" description: @@ -2599,4 +2607,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.20.0-1.2.pre" + flutter: ">=3.22.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index dfdee3e30a..252d1a9842 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -141,10 +141,11 @@ dependencies: provider: ^6.0.0 quiver: ^3.0.1 receive_sharing_intent: ^1.7.0 + screenshot: ^3.0.0 scrollable_positioned_list: ^0.3.5 sentry: ^7.9.0 sentry_flutter: ^7.9.0 - share_plus: 7.2.2 + share_plus: ^9.0.0 shared_preferences: ^2.0.5 simple_cluster: ^0.3.0 sqflite: ^2.3.0