From 30a3668eec89f60dc64605e5230b18cc2718366c Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 11 May 2025 13:12:47 +0200 Subject: [PATCH] fix #155 --- lib/src/utils/misc.dart | 13 ++ .../camera/components/save_to_gallery.dart | 36 ++++-- .../views/camera/share_image_editor_view.dart | 1 + .../views/chats/chat_item_details_view.dart | 120 +++++++++++++----- lib/src/views/chats/media_viewer_view.dart | 21 ++- 5 files changed, 145 insertions(+), 46 deletions(-) diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 1184d1b..e4442d0 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -56,6 +56,19 @@ Future saveImageToGallery(Uint8List imageBytes) async { } } +Future saveVideoToGallery(String videoPath) async { + final hasAccess = await Gal.hasAccess(); + if (!hasAccess) { + await Gal.requestAccess(); + } + try { + await Gal.putVideo(videoPath); + return null; + } on GalException catch (e) { + return e.type.message; + } +} + Uint8List getRandomUint8List(int length) { final Random random = Random.secure(); final Uint8List randomBytes = Uint8List(length); diff --git a/lib/src/views/camera/components/save_to_gallery.dart b/lib/src/views/camera/components/save_to_gallery.dart index 8613dba..987edab 100644 --- a/lib/src/views/camera/components/save_to_gallery.dart +++ b/lib/src/views/camera/components/save_to_gallery.dart @@ -1,3 +1,4 @@ +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'dart:typed_data'; @@ -7,12 +8,13 @@ import 'package:twonly/src/utils/misc.dart'; class SaveToGalleryButton extends StatefulWidget { final Future Function() getMergedImage; final String? sendNextMediaToUserName; + final XFile? videoFilePath; - const SaveToGalleryButton({ - super.key, - required this.getMergedImage, - this.sendNextMediaToUserName, - }); + const SaveToGalleryButton( + {super.key, + required this.getMergedImage, + this.sendNextMediaToUserName, + this.videoFilePath}); @override State createState() => SaveToGalleryButtonState(); @@ -37,15 +39,31 @@ class SaveToGalleryButtonState extends State { setState(() { _imageSaving = true; }); - Uint8List? imageBytes = await widget.getMergedImage(); - if (imageBytes == null || !context.mounted) return; - final res = await saveImageToGallery(imageBytes); + + String? res; + + if (widget.videoFilePath != null) { + res = await saveVideoToGallery(widget.videoFilePath!.path); + } else { + Uint8List? imageBytes = await widget.getMergedImage(); + if (imageBytes == null || !context.mounted) return; + res = await saveImageToGallery(imageBytes); + } if (res == null) { setState(() { - _imageSaving = false; _imageSaved = true; }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(res), + duration: Duration(seconds: 3), + ), + ); } + setState(() { + _imageSaving = false; + }); }, child: Row( children: [ diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index cb8f121..0017a1f 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -501,6 +501,7 @@ class _ShareImageEditorView extends State { children: [ SaveToGalleryButton( getMergedImage: getMergedImage, + videoFilePath: widget.videoFilePath, sendNextMediaToUserName: sendNextMediaToUserName, ), if (sendNextMediaToUserName != null) SizedBox(width: 10), diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index ae183f0..3749526 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -23,32 +23,29 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/contact/contact_view.dart'; +import 'package:video_player/video_player.dart'; -InputDecoration inputTextMessageDeco(BuildContext context) { - return InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: - BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20.0), - borderSide: - BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20.0), - borderSide: BorderSide(color: Colors.grey, width: 2.0), - ), - ); +class ChatMediaViewerFullScreen extends StatelessWidget { + const ChatMediaViewerFullScreen({super.key, required this.message}); + final Message message; + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Center( + child: InChatMediaViewer(message: message, isInFullscreen: true), + ), + ), + ); + } } class InChatMediaViewer extends StatefulWidget { - const InChatMediaViewer({super.key, required this.message}); + const InChatMediaViewer( + {super.key, required this.message, this.isInFullscreen = false}); final Message message; + final bool isInFullscreen; @override State createState() => _InChatMediaViewerState(); @@ -58,6 +55,8 @@ class _InChatMediaViewerState extends State { File? image; File? video; bool isMounted = true; + bool mirrorVideo = false; + VideoPlayerController? videoController; @override void initState() { @@ -73,7 +72,26 @@ class _InChatMediaViewerState extends State { isSend ? "send" : "received", ); if (!isMounted) return; + final videoPath = File("$basePath.mp4"); final imagePath = File("$basePath.png"); + if (videoPath.existsSync() && widget.message.contentJson != null) { + MessageContent? content = MessageContent.fromJson( + MessageKind.media, jsonDecode(widget.message.contentJson!)); + if (content is MediaMessageContent) { + mirrorVideo = content.mirrorVideo; + } + videoController = VideoPlayerController.file(videoPath); + videoController?.initialize().then((_) { + if (!widget.isInFullscreen) { + videoController!.setVolume(0); + } + videoController!.play(); + }); + + setState(() { + image = imagePath; + }); + } if (imagePath.existsSync()) { setState(() { image = imagePath; @@ -87,22 +105,41 @@ class _InChatMediaViewerState extends State { void dispose() { super.dispose(); isMounted = false; + videoController?.dispose(); } @override Widget build(BuildContext context) { - return Stack( - children: [ - if (image != null) Image.file(image!), - if (image == null && video == null) - Padding( - padding: const EdgeInsets.all(10.0), - child: MessageSendStateIcon( - [widget.message], - mainAxisAlignment: MainAxisAlignment.center, + return GestureDetector( + onTap: () { + if (widget.isInFullscreen) return; + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return ChatMediaViewerFullScreen(message: widget.message); + }), + ); + }, + child: Stack( + children: [ + if (image != null) Image.file(image!), + if (videoController != null) + Positioned.fill( + child: Transform.flip( + flipX: mirrorVideo, + child: VideoPlayer(videoController!), + ), ), - ) - ], + if (image == null && video == null) + Padding( + padding: const EdgeInsets.all(10.0), + child: MessageSendStateIcon( + [widget.message], + mainAxisAlignment: MainAxisAlignment.center, + ), + ) + ], + ), ); } } @@ -650,3 +687,24 @@ class _ChatItemDetailsViewState extends State { ); } } + +InputDecoration inputTextMessageDeco(BuildContext context) { + return InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20.0), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20.0), + borderSide: BorderSide(color: Colors.grey, width: 2.0), + ), + ); +} diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index b759087..9b142b6 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -41,6 +41,7 @@ class _MediaViewerViewState extends State { // current image related Uint8List? imageBytes; + String? videoPath; VideoPlayerController? videoController; DateTime? canBeSeenUntil; @@ -137,6 +138,7 @@ class _MediaViewerViewState extends State { imageSaved = false; mirrorVideo = false; progress = 0; + videoPath = null; isDownloading = false; isRealTwonly = false; showSendTextMessageInput = false; @@ -200,9 +202,9 @@ class _MediaViewerViewState extends State { ); if (content.isVideo) { - final vidoePath = await getVideoPath(current.messageId); - if (vidoePath != null) { - videoController = VideoPlayerController.file(File(vidoePath.path)); + final videoPathTmp = await getVideoPath(current.messageId); + if (videoPathTmp != null) { + videoController = VideoPlayerController.file(File(videoPathTmp.path)); videoController?.setLooping(content.maxShowTime == gMediaShowInfinite); videoController?.initialize().then((_) { videoController!.play(); @@ -214,7 +216,9 @@ class _MediaViewerViewState extends State { } }); } - setState(() {}); + setState(() { + videoPath = videoPathTmp.path; + }); }).catchError((Object error) { Logger("media_viewer_view.dart").shout(error); }); @@ -307,8 +311,13 @@ class _MediaViewerViewState extends State { imageSaved = true; }); final user = await getUser(); + if (user != null && (user.storeMediaFilesInGallery ?? true)) { - await saveImageToGallery(imageBytes!); + if (videoPath != null) { + await saveVideoToGallery(videoPath!); + } else { + await saveImageToGallery(imageBytes!); + } } setState(() { imageSaving = false; @@ -320,7 +329,7 @@ class _MediaViewerViewState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (maxShowTime == gMediaShowInfinite && videoController == null) + if (maxShowTime == gMediaShowInfinite) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: imageSaved