From 5e90af79d83354bf1a820f3984054bc8dba684e0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 21 Apr 2025 23:38:07 +0200 Subject: [PATCH] starting with #25 --- ios/Podfile.lock | 7 + lib/src/views/camera/camera_preview_view.dart | 574 ++++++++++-------- .../camera/components/save_to_gallery.dart | 69 +++ .../views/camera/share_image_editor_view.dart | 195 +++--- pubspec.lock | 56 ++ pubspec.yaml | 1 + 6 files changed, 555 insertions(+), 347 deletions(-) create mode 100644 lib/src/views/camera/components/save_to_gallery.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 130b615..5a10eb4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -216,6 +216,9 @@ PODS: - sqlite3/rtree - url_launcher_ios (0.0.1): - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) @@ -247,6 +250,7 @@ DEPENDENCIES: - sqlite3 - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: trunk: @@ -315,6 +319,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 @@ -357,6 +363,7 @@ SPEC CHECKSUMS: sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b PODFILE CHECKSUM: 4d78ee29daee4dd5268f87f2e6b41e472cc27728 diff --git a/lib/src/views/camera/camera_preview_view.dart b/lib/src/views/camera/camera_preview_view.dart index a0e09a7..48c9f53 100644 --- a/lib/src/views/camera/camera_preview_view.dart +++ b/lib/src/views/camera/camera_preview_view.dart @@ -5,6 +5,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:logging/logging.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/views/camera/components/zoom_selector.dart'; @@ -37,6 +39,9 @@ class _CameraPreviewViewState extends State { double baseScaleFactor = 0; bool cameraLoaded = false; bool useHighQuality = false; + bool isVideoRecording = false; + bool hasAudioPermission = true; + final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); late CameraController controller; @@ -64,10 +69,10 @@ class _CameraPreviewViewState extends State { final user = await getUser(); if (user == null) return; if (user.useHighQuality != null) { - setState(() { - useHighQuality = user.useHighQuality!; - }); + useHighQuality = user.useHighQuality!; } + hasAudioPermission = await Permission.microphone.isGranted; + setState(() {}); } @override @@ -79,6 +84,18 @@ class _CameraPreviewViewState extends State { super.dispose(); } + Future requestMicrophonePermission() async { + Map statuses = await [ + Permission.microphone, + ].request(); + if (statuses[Permission.microphone]!.isPermanentlyDenied) { + openAppSettings(); + } else { + hasAudioPermission = await Permission.microphone.isGranted; + setState(() {}); + } + } + void selectCamera(int sCameraId, {bool init = false}) { if (sCameraId >= gCameras.length) return; if (init) { @@ -181,18 +198,7 @@ class _CameraPreviewViewState extends State { final XFile picture = await controller.takePicture(); imageBytes = loadAndDeletePictureFromFile(picture); } catch (e) { - try { - if (context.mounted) { - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error taking picture: $e'), - duration: Duration(seconds: 3), - ), - ); - } - // ignore: empty_catches - } catch (e) {} + _showCameraException(e); return; } } else { @@ -215,31 +221,22 @@ class _CameraPreviewViewState extends State { imageBytes = screenshotController.capture(pixelRatio: 1); } - if (await pushImageEditor(imageBytes)) { + if (await pushMediaEditor(imageBytes, null)) { return; } - - // does not work?? - // if (Platform.isIOS) { - // await controller.resumePreview(); - // } else { - selectCamera(cameraId); - // } - if (context.mounted) { - setState(() { - sharePreviewIsShown = false; - showSelfieFlash = false; - }); - } } - Future pushImageEditor(Future imageBytes) async { + Future pushMediaEditor( + Future? imageBytes, XFile? videFilePath) async { bool? shoudReturn = await Navigator.push( context, PageRouteBuilder( opaque: false, - pageBuilder: (context, a1, a2) => - ShareImageEditorView(imageBytes: imageBytes, sendTo: widget.sendTo), + pageBuilder: (context, a1, a2) => ShareImageEditorView( + videFilePath: videFilePath, + imageBytes: imageBytes, + sendTo: widget.sendTo, + ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; }, @@ -254,6 +251,13 @@ class _CameraPreviewViewState extends State { Navigator.pop(context); return true; } + selectCamera(cameraId); + if (context.mounted) { + setState(() { + sharePreviewIsShown = false; + showSelfieFlash = false; + }); + } return false; } @@ -261,18 +265,93 @@ class _CameraPreviewViewState extends State { controller.description.lensDirection == CameraLensDirection.front; Future onPanUpdate(details) async { + print(details); if (isFront) { return; } var diff = basePanY - details.localPosition.dy; - if (diff > 200) diff = 200; - if (diff < -200) diff = -200; - var tmp = (diff / 200 * (7 * 2)).toInt() / 2; + + var baseDiff = Platform.isAndroid ? 200.0 : 300.0; + + if (diff > baseDiff) diff = baseDiff; + if (diff < -baseDiff) diff = -baseDiff; + var tmp = (diff / baseDiff * (14 * 2)).toInt() / 4; tmp = baseScaleFactor + tmp; if (tmp < 1) tmp = 1; updateScaleFactor(tmp); } + Future pickImageFromGallery() async { + setState(() { + galleryLoadedImageIsShown = true; + sharePreviewIsShown = true; + }); + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + + if (pickedFile != null) { + File imageFile = File(pickedFile.path); + if (await pushMediaEditor(imageFile.readAsBytes(), null)) { + return; + } + } + setState(() { + galleryLoadedImageIsShown = false; + sharePreviewIsShown = false; + }); + } + + Future startVideoRecording() async { + if (controller.value.isRecordingVideo) return; + + try { + await controller.startVideoRecording(); + setState(() { + isVideoRecording = true; + }); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + if (!controller.value.isRecordingVideo) { + return null; + } + + try { + setState(() { + isVideoRecording = false; + sharePreviewIsShown = true; + }); + XFile? videoPath = await controller.stopVideoRecording(); + await controller.pausePreview(); + if (await pushMediaEditor(null, videoPath)) { + return; + } + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(dynamic e) { + Logger("ui.camera").shout("$e"); + try { + if (context.mounted) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + duration: Duration(seconds: 3), + ), + ); + } + // ignore: empty_catches + } catch (e) {} + } + @override Widget build(BuildContext context) { if (cameraId >= gCameras.length) { @@ -281,224 +360,239 @@ class _CameraPreviewViewState extends State { ); } return MediaViewSizing( - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(22), - child: Stack( - children: [ - if (!galleryLoadedImageIsShown) - CameraPreviewWidget( - controller: controller, - screenshotController: screenshotController, - isFront: isFront, - ), - if (galleryLoadedImageIsShown) - Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 1, color: context.color.primary), + child: GestureDetector( + onPanStart: (details) async { + if (isFront) { + return; + } + setState(() { + basePanY = details.localPosition.dy; + baseScaleFactor = scaleFactor; + }); + }, + onDoubleTap: () async { + selectCamera((cameraId + 1) % 2); + }, + onLongPressMoveUpdate: onPanUpdate, + onLongPressStart: (details) { + setState(() { + basePanY = details.localPosition.dy; + baseScaleFactor = scaleFactor; + }); + print("onLongPressDown"); + // Get the position of the pointer + RenderBox renderBox = + keyTriggerButton.currentContext?.findRenderObject() as RenderBox; + Offset localPosition = + renderBox.globalToLocal(details.globalPosition); + + final containerRect = + Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); + + if (containerRect.contains(localPosition)) { + startVideoRecording(); + } + }, + onLongPressEnd: (a) { + stopVideoRecording(); + }, + onPanUpdate: onPanUpdate, + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Stack( + children: [ + if (!galleryLoadedImageIsShown) + CameraPreviewWidget( + controller: controller, + screenshotController: screenshotController, + isFront: isFront, ), - ), - Positioned.fill( - child: GestureDetector( - onPanStart: (details) async { - if (isFront) { - return; - } - setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = scaleFactor; - }); - }, - onPanUpdate: onPanUpdate, - onDoubleTap: () async { - selectCamera((cameraId + 1) % 2); - }, - ), - ), - if (!sharePreviewIsShown && widget.sendTo != null) - SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), - if (!sharePreviewIsShown) - Positioned( - right: 5, - top: 0, - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.symmetric(vertical: 16), - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ActionButton( - FontAwesomeIcons.repeat, - tooltipText: - context.lang.switchFrontAndBackCamera, - onPressed: () async { - selectCamera((cameraId + 1) % 2); - }, - ), - ActionButton( - isFlashOn - ? Icons.flash_on_rounded - : Icons.flash_off_rounded, - tooltipText: context.lang.toggleFlashLight, - color: isFlashOn - ? Colors.white - : Color.fromARGB(158, 255, 255, 255), - onPressed: () async { - if (isFlashOn) { - controller.setFlashMode(FlashMode.off); - isFlashOn = false; - } else { - controller.setFlashMode(FlashMode.always); - isFlashOn = true; - } - setState(() {}); - }, - ), - if (!isFront) + if (galleryLoadedImageIsShown) + Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 1, color: context.color.primary), + ), + ), + // Positioned.fill( + // child: GestureDetector(), + // ), + if (!sharePreviewIsShown && widget.sendTo != null) + SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), + if (!sharePreviewIsShown && !isVideoRecording) + Positioned( + right: 5, + top: 0, + child: Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(vertical: 16), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ ActionButton( - Icons.hd_rounded, - tooltipText: context.lang.toggleHighQuality, - color: useHighQuality - ? Colors.white - : const Color.fromARGB(158, 255, 255, 255), + Icons.repeat_rounded, + tooltipText: + context.lang.switchFrontAndBackCamera, onPressed: () async { - useHighQuality = !useHighQuality; - setState(() {}); - var user = await getUser(); - if (user != null) { - user.useHighQuality = useHighQuality; - updateUser(user); - } + selectCamera((cameraId + 1) % 2); }, ), + ActionButton( + isFlashOn + ? Icons.flash_on_rounded + : Icons.flash_off_rounded, + tooltipText: context.lang.toggleFlashLight, + color: isFlashOn + ? Colors.white + : Colors.white.withAlpha(160), + onPressed: () async { + if (isFlashOn) { + controller.setFlashMode(FlashMode.off); + isFlashOn = false; + } else { + controller.setFlashMode(FlashMode.always); + isFlashOn = true; + } + setState(() {}); + }, + ), + if (!isFront) + ActionButton( + Icons.hd_rounded, + tooltipText: context.lang.toggleHighQuality, + color: useHighQuality + ? Colors.white + : Colors.white.withAlpha(160), + onPressed: () async { + useHighQuality = !useHighQuality; + setState(() {}); + var user = await getUser(); + if (user != null) { + user.useHighQuality = useHighQuality; + updateUser(user); + } + }, + ), + if (!hasAudioPermission) + ActionButton( + Icons.mic_off_rounded, + color: Colors.white.withAlpha(160), + tooltipText: + "Allow microphone access for video recording.", + onPressed: requestMicrophonePermission, + ) + ], + ), + ), + ), + ), + if (!sharePreviewIsShown) + Positioned( + bottom: 30, + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + child: Column( + children: [ + if (controller.value.isInitialized && + isZoomAble && + !isFront && + !isVideoRecording) + SizedBox( + width: 120, + child: CameraZoomButtons( + key: widget.key, + scaleFactor: scaleFactor, + updateScaleFactor: updateScaleFactor, + controller: controller, + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!isVideoRecording) + GestureDetector( + onTap: pickImageFromGallery, + child: Align( + alignment: Alignment.center, + child: Container( + height: 50, + width: 80, + padding: const EdgeInsets.all(2), + child: Center( + child: FaIcon( + FontAwesomeIcons.photoFilm, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ), + GestureDetector( + onTap: takePicture, + // onLongPress: startVideoRecording, + key: keyTriggerButton, + child: Align( + alignment: Alignment.center, + child: Container( + height: 100, + width: 100, + clipBehavior: Clip.antiAliasWithSaveLayer, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 7, + color: isVideoRecording + ? Colors.red + : Colors.white, + ), + ), + ), + ), + ), + if (!isVideoRecording) SizedBox(width: 80) + ], + ), ], ), ), ), - ), - if (!sharePreviewIsShown) - Positioned( - bottom: 30, - left: 0, - right: 0, - child: Align( - alignment: Alignment.bottomCenter, - child: Column( - children: [ - if (controller.value.isInitialized && - isZoomAble && - !isFront) - SizedBox( - width: 120, - child: CameraZoomButtons( - key: widget.key, - scaleFactor: scaleFactor, - updateScaleFactor: updateScaleFactor, - controller: controller, - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () async { - setState(() { - galleryLoadedImageIsShown = true; - sharePreviewIsShown = true; - }); - final picker = ImagePicker(); - final pickedFile = await picker.pickImage( - source: ImageSource.gallery); - - if (pickedFile != null) { - File imageFile = File(pickedFile.path); - if (await pushImageEditor( - imageFile.readAsBytes())) { - return; - } - } - setState(() { - galleryLoadedImageIsShown = false; - sharePreviewIsShown = false; - }); - }, - child: Align( - alignment: Alignment.center, - child: Container( - height: 50, - width: 80, - padding: const EdgeInsets.all(2), - child: Center( - child: FaIcon( - FontAwesomeIcons.photoFilm, - color: Colors.white, - size: 25, - ), - ), - ), - ), - ), - GestureDetector( - onTap: () async { - takePicture(); - }, - onLongPress: () async {}, - child: Align( - alignment: Alignment.center, - child: Container( - height: 100, - width: 100, - clipBehavior: Clip.antiAliasWithSaveLayer, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 7, - color: Colors.white, - ), - ), - ), - ), - ), - SizedBox(width: 80) - ], - ), - ], - ), - ), - ), - ], - ), - ), - if (!sharePreviewIsShown && widget.sendTo != null) - Positioned( - left: 5, - top: 10, - child: ActionButton( - FontAwesomeIcons.xmark, - tooltipText: context.lang.close, - onPressed: () async { - Navigator.pop(context); - }, + ], ), ), - if (showSelfieFlash) - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(22), - child: Container( - color: Colors.white, + if (!sharePreviewIsShown && widget.sendTo != null) + Positioned( + left: 5, + top: 10, + child: ActionButton( + FontAwesomeIcons.xmark, + tooltipText: context.lang.close, + onPressed: () async { + Navigator.pop(context); + }, ), ), - ), - ], + if (showSelfieFlash) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Container( + color: Colors.white, + ), + ), + ), + ], + ), ), ); } diff --git a/lib/src/views/camera/components/save_to_gallery.dart b/lib/src/views/camera/components/save_to_gallery.dart new file mode 100644 index 0000000..8613dba --- /dev/null +++ b/lib/src/views/camera/components/save_to_gallery.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'dart:typed_data'; + +import 'package:twonly/src/utils/misc.dart'; + +class SaveToGalleryButton extends StatefulWidget { + final Future Function() getMergedImage; + final String? sendNextMediaToUserName; + + const SaveToGalleryButton({ + super.key, + required this.getMergedImage, + this.sendNextMediaToUserName, + }); + + @override + State createState() => SaveToGalleryButtonState(); +} + +class SaveToGalleryButtonState extends State { + bool _imageSaving = false; + bool _imageSaved = false; + + @override + Widget build(BuildContext context) { + return OutlinedButton( + style: OutlinedButton.styleFrom( + iconColor: _imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + foregroundColor: _imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + ), + onPressed: () async { + setState(() { + _imageSaving = true; + }); + Uint8List? imageBytes = await widget.getMergedImage(); + if (imageBytes == null || !context.mounted) return; + final res = await saveImageToGallery(imageBytes); + if (res == null) { + setState(() { + _imageSaving = false; + _imageSaved = true; + }); + } + }, + child: Row( + children: [ + _imageSaving + ? SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator(strokeWidth: 1)) + : _imageSaved + ? Icon(Icons.check) + : FaIcon(FontAwesomeIcons.floppyDisk), + if (widget.sendNextMediaToUserName == null) SizedBox(width: 10), + if (widget.sendNextMediaToUserName == null) + Text(_imageSaved + ? context.lang.shareImagedEditorSavedImage + : context.lang.shareImagedEditorSaveImage) + ], + ), + ); + } +} diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 0046f95..852514a 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/views/camera/components/save_to_gallery.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; @@ -17,6 +21,7 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:video_player/video_player.dart'; List layers = []; List undoLayers = []; @@ -24,8 +29,9 @@ List removedLayers = []; class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView( - {super.key, required this.imageBytes, this.sendTo}); - final Future imageBytes; + {super.key, this.imageBytes, this.sendTo, this.videFilePath}); + final Future? imageBytes; + final XFile? videFilePath; final Contact? sendTo; @override State createState() => _ShareImageEditorView(); @@ -33,13 +39,13 @@ class ShareImageEditorView extends StatefulWidget { class _ShareImageEditorView extends State { bool imageLoadedReady = false; - bool _imageSaved = false; - bool _imageSaving = false; bool _isRealTwonly = false; int maxShowTime = 999999; String? sendNextMediaToUserName; double tabDownPostion = 0; bool sendingImage = false; + double widthRatio = 1, heightRatio = 1, pixelRatio = 1; + VideoPlayerController? videoController; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -48,7 +54,25 @@ class _ShareImageEditorView extends State { void initState() { super.initState(); initAsync(); - loadImage(widget.imageBytes); + if (widget.imageBytes != null) { + loadImage(widget.imageBytes!); + } else if (widget.videFilePath != null) { + videoController = + VideoPlayerController.file(File(widget.videFilePath!.path)); + videoController?.addListener(() { + setState(() {}); + }); + videoController?.setLooping(true); + videoController?.initialize().then((_) { + videoController!.play(); + + setState(() {}); + }).catchError((Object error) { + print(error); + }); + videoController?.play(); + print(widget.videFilePath!.path); + } } void initAsync() async { @@ -64,6 +88,7 @@ class _ShareImageEditorView extends State { @override void dispose() { layers.clear(); + videoController?.dispose(); super.dispose(); } @@ -221,7 +246,23 @@ class _ShareImageEditorView extends State { ]; } - double widthRatio = 1, heightRatio = 1, pixelRatio = 1; + Future pushShareImageView() async { + Future imageBytes = getMergedImage(); + bool? wasSend = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageView( + imageBytesFuture: imageBytes, + isRealTwonly: _isRealTwonly, + maxShowTime: maxShowTime, + preselectedUser: widget.sendTo, + ), + ), + ); + if (wasSend != null && wasSend && context.mounted) { + Navigator.pop(context, true); + } + } Future getMergedImage() async { Uint8List? image; @@ -263,6 +304,26 @@ class _ShareImageEditorView extends State { }); } + Future sendImageToSinglePerson() async { + setState(() { + sendingImage = true; + }); + Uint8List? imageBytes = await getMergedImage(); + if (!context.mounted) return; + if (imageBytes == null) { + // ignore: use_build_context_synchronously + Navigator.pop(context, false); + return; + } + sendImage( + [widget.sendTo!.userId], + imageBytes, + _isRealTwonly, + maxShowTime, + ); + Navigator.pop(context, true); + } + @override Widget build(BuildContext context) { pixelRatio = MediaQuery.of(context).devicePixelRatio; @@ -301,14 +362,20 @@ class _ShareImageEditorView extends State { child: SizedBox( height: currentImage.height / pixelRatio, width: currentImage.width / pixelRatio, - child: Screenshot( - controller: screenshotController, - child: LayersViewer( - layers: layers.where((x) => !x.isDeleted).toList(), - onUpdate: () { - setState(() {}); - }, - ), + child: Stack( + children: [ + if (videoController != null) + Positioned.fill(child: VideoPlayer(videoController!)), + Screenshot( + controller: screenshotController, + child: LayersViewer( + layers: layers.where((x) => !x.isDeleted).toList(), + onUpdate: () { + setState(() {}); + }, + ), + ), + ], ), ), ), @@ -347,46 +414,9 @@ class _ShareImageEditorView extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - OutlinedButton( - style: OutlinedButton.styleFrom( - iconColor: _imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - foregroundColor: _imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - ), - onPressed: () async { - setState(() { - _imageSaving = true; - }); - Uint8List? imageBytes = await getMergedImage(); - if (imageBytes == null || !context.mounted) return; - final res = await saveImageToGallery(imageBytes); - if (res == null) { - setState(() { - _imageSaving = false; - _imageSaved = true; - }); - } - }, - child: Row( - children: [ - _imageSaving - ? SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator(strokeWidth: 1)) - : _imageSaved - ? Icon(Icons.check) - : FaIcon(FontAwesomeIcons.floppyDisk), - if (sendNextMediaToUserName == null) SizedBox(width: 10), - if (sendNextMediaToUserName == null) - Text(_imageSaved - ? context.lang.shareImagedEditorSavedImage - : context.lang.shareImagedEditorSaveImage) - ], - ), + SaveToGalleryButton( + getMergedImage: getMergedImage, + sendNextMediaToUserName: sendNextMediaToUserName, ), if (sendNextMediaToUserName != null) SizedBox(width: 10), if (sendNextMediaToUserName != null) @@ -395,27 +425,10 @@ class _ShareImageEditorView extends State { iconColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.primary, ), - onPressed: () async { - Future imageBytes = getMergedImage(); - bool? wasSend = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageView( - imageBytesFuture: imageBytes, - isRealTwonly: _isRealTwonly, - maxShowTime: maxShowTime, - preselectedUser: widget.sendTo, - ), - ), - ); - if (wasSend != null && wasSend && context.mounted) { - Navigator.pop(context, true); - } - }, + onPressed: pushShareImageView, child: FaIcon(FontAwesomeIcons.userPlus), ), - if (sendNextMediaToUserName != null) SizedBox(width: 10), - if (sendNextMediaToUserName == null) SizedBox(width: 20), + SizedBox(width: sendNextMediaToUserName == null ? 20 : 10), FilledButton.icon( icon: sendingImage ? SizedBox( @@ -429,40 +442,8 @@ class _ShareImageEditorView extends State { : FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { if (sendingImage) return; - if (widget.sendTo != null) { - setState(() { - sendingImage = true; - }); - Uint8List? imageBytes = await getMergedImage(); - if (!context.mounted) return; - if (imageBytes == null) { - Navigator.pop(context, false); - return; - } - sendImage( - [widget.sendTo!.userId], - imageBytes, - _isRealTwonly, - maxShowTime, - ); - Navigator.pop(context, true); - return; - } - Future imageBytes = getMergedImage(); - bool? wasSend = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageView( - imageBytesFuture: imageBytes, - isRealTwonly: _isRealTwonly, - maxShowTime: maxShowTime, - preselectedUser: widget.sendTo, - ), - ), - ); - if (wasSend != null && wasSend && context.mounted) { - Navigator.pop(context, true); - } + if (widget.sendTo == null) return pushShareImageView(); + sendImageToSinglePerson(); }, style: ButtonStyle( padding: WidgetStateProperty.all( diff --git a/pubspec.lock b/pubspec.lock index b47f78b..9bdff08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -313,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.1" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -796,6 +804,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + html: + dependency: transitive + description: + name: html + sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8" + url: "https://pub.dev" + source: hosted + version: "0.15.5+1" http: dependency: "direct main" description: @@ -1753,6 +1769,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" + url: "https://pub.dev" + source: hosted + version: "2.9.5" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a + url: "https://pub.dev" + source: hosted + version: "2.8.2" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" + url: "https://pub.dev" + source: hosted + version: "2.7.1" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" + url: "https://pub.dev" + source: hosted + version: "2.3.4" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 29c4a2f..59128fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: image_picker: ^1.1.2 http: ^1.3.0 get: ^4.7.2 + video_player: ^2.9.5 # avatar_maker # avatar_maker: # path: ./dependencies/avatar_maker/