import 'dart:async'; import "dart:io"; import "dart:typed_data"; import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import "package:photo_manager/photo_manager.dart"; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import "package:photos/db/files_db.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/collection/collection.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import "package:photos/ui/sharing/show_images_prevew.dart"; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:photos/utils/standalone/date_time.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import "package:screenshot/screenshot.dart"; import 'package:share_plus/share_plus.dart'; import "package:uuid/uuid.dart"; final _logger = Logger("ShareUtil"); /// share is used to share media/files from ente to other apps Future share( BuildContext context, List files, { GlobalKey? shareButtonKey, }) async { final remoteFileCount = files.where((element) => element.isRemoteFile).length; final dialog = createProgressDialog( context, "Preparing...", isDismissible: remoteFileCount > 2, ); await dialog.show(); try { final List> pathFutures = []; for (EnteFile file in files) { // Note: We are requesting the origin file for performance reasons on iOS. // This will eat up storage, which will be reset only when the app restarts. // We could have cleared the cache had there been a callback to the share API. pathFutures.add( getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile?.path), ); } final paths = await Future.wait(pathFutures); await dialog.hide(); paths.removeWhere((element) => element == null); 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), ); } catch (e, s) { _logger.severe( "failed to fetch files for system share ${files.length}", e, s, ); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); } } /// 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.sizeOf(context); final RenderObject? renderObject = shareButtonKey?.currentContext?.findRenderObject(); RenderBox? renderBox; if (renderObject != null && renderObject is RenderBox) { renderBox = renderObject; } if (renderBox == null) { return Rect.fromLTWH(0, 0, size.width, size.height / 2); } size = renderBox.size; final Offset position = renderBox.localToGlobal(Offset.zero); return Rect.fromCenter( center: position + Offset(size.width / 2, size.height / 2), width: size.width, height: size.height, ); } 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( List sharedMedia, int collectionID, ) async { final List localFiles = []; for (var media in sharedMedia) { if (!(media.type == SharedMediaType.image || media.type == SharedMediaType.video)) { _logger.warning( "ignore unsupported file type ${media.type.toString()} path: ${media.path}", ); continue; } final enteFile = EnteFile(); final sharedLocalId = const Uuid().v4(); // fileName: img_x.jpg enteFile.title = basename(media.path); var ioFile = File(media.path); try { ioFile = ioFile.renameSync( Configuration.instance.getSharedMediaDirectory() + "/" + sharedLocalId, ); } catch (e) { if (e is FileSystemException) { //from renameSync docs: //On some platforms, a rename operation cannot move a file between //different file systems. If that is the case, instead copySync the //file to the new location and then deleteSync the original. _logger.info("Creating new copy of file in path ${ioFile.path}"); final newIoFile = ioFile.copySync( Configuration.instance.getSharedMediaDirectory() + "/" + sharedLocalId, ); if (media.path.contains("io.ente.photos")) { _logger.info("delete original file in path ${ioFile.path}"); ioFile.deleteSync(); } ioFile = newIoFile; } else { rethrow; } } enteFile.localID = sharedMediaIdentifier + sharedLocalId; enteFile.collectionID = collectionID; enteFile.fileType = media.type == SharedMediaType.image ? FileType.image : FileType.video; if (enteFile.fileType == FileType.image) { final dateResult = await tryParseExifDateTime(ioFile, null); if (dateResult != null && dateResult.time != null) { enteFile.creationTime = dateResult.time!.microsecondsSinceEpoch; } } else if (enteFile.fileType == FileType.video) { enteFile.duration = (media.duration ?? 0) ~/ 1000; } if (enteFile.creationTime == null || enteFile.creationTime == 0) { final parsedDateTime = parseDateTimeFromFileNameV2(basenameWithoutExtension(media.path)); if (parsedDateTime != null) { enteFile.creationTime = parsedDateTime.microsecondsSinceEpoch; } else { enteFile.creationTime = DateTime.now().microsecondsSinceEpoch; } } enteFile.modificationTime = enteFile.creationTime; enteFile.metadataVersion = EnteFile.kCurrentMetadataVersion; localFiles.add(enteFile); } return localFiles; } Future> convertPicketAssets( List pickedAssets, int collectionID, ) async { final List localFiles = []; for (var asset in pickedAssets) { final enteFile = await EnteFile.fromAsset('', asset); enteFile.collectionID = collectionID; localFiles.add(enteFile); } return localFiles; } DateTime? parseDateFromFileNam1e(String fileName) { if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) { // Whatsapp media files return DateTime.tryParse(fileName.split('-')[1]); } else if (fileName.startsWith("Screenshot_")) { // Screenshots on droid return DateTime.tryParse( (fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'), ); } else { return DateTime.tryParse( (fileName) .replaceAll("IMG_", "") .replaceAll("VID_", "") .replaceAll("DCIM_", "") .replaceAll("_", " "), ); } } void shareSelected( BuildContext context, GlobalKey shareButtonKey, List selectedFiles, ) { share( context, selectedFiles.toList(), shareButtonKey: shareButtonKey, ); } Future shareImageAndUrl( Uint8List imageBytes, String url, { BuildContext? context, GlobalKey? key, }) async { final sharePosOrigin = _sharePosOrigin(context, key); await Share.shareXFiles( [ XFile.fromData( imageBytes, name: 'placeholder_image.png', mimeType: 'image/png', ), ], text: url, sharePositionOrigin: sharePosOrigin, ); } Future shareAlbumLinkWithPlaceholder( BuildContext context, Collection collection, String url, GlobalKey key, ) async { final ScreenshotController screenshotController = ScreenshotController(); final List filesInCollection = (await FilesDB.instance.getFilesInCollection( collection.id, galleryLoadStartTime, galleryLoadEndTime, )) .files; final dialog = createProgressDialog( context, S.of(context).creatingLink, isDismissible: true, ); await dialog.show(); if (filesInCollection.isEmpty) { await dialog.hide(); await shareText(url); return; } else { final placeholderBytes = await _createAlbumPlaceholder( filesInCollection, screenshotController, context, ); await dialog.hide(); await shareImageAndUrl( placeholderBytes, url, context: context, key: key, ); } } /// 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; } Future _createAlbumPlaceholder( List files, ScreenshotController screenshotController, BuildContext context, ) async { final Widget imageWidget = LinkPlaceholder( files: files, ); final double pixelRatio = MediaQuery.devicePixelRatioOf(context); final bytesOfImageToWidget = await screenshotController.captureFromWidget( imageWidget, pixelRatio: pixelRatio, targetSize: MediaQuery.sizeOf(context), delay: const Duration(milliseconds: 300), ); return bytesOfImageToWidget; }