diff --git a/lib/src/database/tables/media_uploads_table.dart b/lib/src/database/tables/media_uploads_table.dart index d6a6239..0da803e 100644 --- a/lib/src/database/tables/media_uploads_table.dart +++ b/lib/src/database/tables/media_uploads_table.dart @@ -47,6 +47,7 @@ class MediaUploadMetadata { late DateTime messageSendAt; late bool isVideo; late bool videoWithAudio; + late bool mirrorVideo; MediaUploadMetadata(); @@ -54,6 +55,7 @@ class MediaUploadMetadata { return { 'contactIds': contactIds, 'isRealTwonly': isRealTwonly, + 'mirrorVideo': mirrorVideo, 'maxShowTime': maxShowTime, 'isVideo': isVideo, 'videoWithAudio': videoWithAudio, @@ -67,6 +69,7 @@ class MediaUploadMetadata { state.isRealTwonly = json['isRealTwonly']; state.videoWithAudio = json['videoWithAudio']; state.isVideo = json['isVideo']; + state.mirrorVideo = json['mirrorVideo']; state.maxShowTime = json['maxShowTime']; state.maxShowTime = json['maxShowTime']; state.messageSendAt = DateTime.parse(json['messageSendAt']); diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index abdcd87..fffec32 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -99,6 +99,7 @@ class MediaMessageContent extends MessageContent { final int maxShowTime; final bool isRealTwonly; final bool isVideo; + final bool mirrorVideo; final List? downloadToken; final List? encryptionKey; final List? encryptionMac; @@ -108,6 +109,7 @@ class MediaMessageContent extends MessageContent { required this.maxShowTime, required this.isRealTwonly, required this.isVideo, + required this.mirrorVideo, this.downloadToken, this.encryptionKey, this.encryptionMac, @@ -131,6 +133,7 @@ class MediaMessageContent extends MessageContent { maxShowTime: json['maxShowTime'], isRealTwonly: json['isRealTwonly'], isVideo: json['isVideo'] ?? false, + mirrorVideo: json['mirrorVideo'] ?? false, ); } @@ -144,6 +147,7 @@ class MediaMessageContent extends MessageContent { 'isRealTwonly': isRealTwonly, 'maxShowTime': maxShowTime, 'isVideo': isVideo, + 'mirrorVideo': mirrorVideo, }; } } diff --git a/lib/src/providers/api/media_send.dart b/lib/src/providers/api/media_send.dart index 0809f4a..7882c4d 100644 --- a/lib/src/providers/api/media_send.dart +++ b/lib/src/providers/api/media_send.dart @@ -20,13 +20,13 @@ import 'package:twonly/src/services/notification_service.dart'; import 'package:video_compress/video_compress.dart'; Future sendMediaFile( - List userIds, - Uint8List imageBytes, - bool isRealTwonly, - int maxShowTime, - XFile? videoFilePath, - bool? enableVideoAudio, -) async { + List userIds, + Uint8List imageBytes, + bool isRealTwonly, + int maxShowTime, + XFile? videoFilePath, + bool? enableVideoAudio, + bool mirrorVideo) async { MediaUploadMetadata metadata = MediaUploadMetadata(); metadata.contactIds = userIds; metadata.isRealTwonly = isRealTwonly; @@ -34,6 +34,7 @@ Future sendMediaFile( metadata.isVideo = videoFilePath != null; metadata.videoWithAudio = enableVideoAudio != null && enableVideoAudio; metadata.maxShowTime = maxShowTime; + metadata.mirrorVideo = mirrorVideo; int? mediaUploadId = await twonlyDatabase.mediaUploadsDao.insertMediaUpload( MediaUploadsCompanion( @@ -62,62 +63,62 @@ Future retryMediaUpload() async { final lockingHandleMediaFile = Mutex(); Future handleSingleMediaFile(int mediaUploadId) async { - // await lockingHandleMediaFile.protect(() async { - MediaUpload? media = await twonlyDatabase.mediaUploadsDao - .getMediaUploadById(mediaUploadId) - .getSingleOrNull(); - if (media == null) return; + await lockingHandleMediaFile.protect(() async { + MediaUpload? media = await twonlyDatabase.mediaUploadsDao + .getMediaUploadById(mediaUploadId) + .getSingleOrNull(); + if (media == null) return; - try { - switch (media.state) { - case UploadState.pending: - await handleAddToMessageDb(media); - break; - case UploadState.addedToMessagesDb: - await handleCompressionState(media); - break; - case UploadState.isCompressed: - await handleEncryptionState(media); - break; - case UploadState.isEncrypted: - if (!await handleGetUploadToken(media)) { - return; // recoverable error. try again when connected again to the server... - } - break; - case UploadState.hasUploadToken: - if (!await handleUpload(media)) { - return; // recoverable error. try again when connected again to the server... - } - break; - case UploadState.isUploaded: - if (!await handleNotifyReceiver(media)) { - return; // recoverable error. try again when connected again to the server... - } - break; - case UploadState.receiverNotified: - return; - } - - // this will be called until there is an recoverable error OR - // the upload is ready - await handleSingleMediaFile(mediaUploadId); - } catch (e) { - // if the messageIds are already there notify the user about this error... - if (media.messageIds != null) { - for (int messageId in media.messageIds!) { - await twonlyDatabase.messagesDao.updateMessageByMessageId( - messageId, - MessagesCompanion( - errorWhileSending: Value(true), - ), - ); + try { + switch (media.state) { + case UploadState.pending: + await handleAddToMessageDb(media); + break; + case UploadState.addedToMessagesDb: + await handleCompressionState(media); + break; + case UploadState.isCompressed: + await handleEncryptionState(media); + break; + case UploadState.isEncrypted: + if (!await handleGetUploadToken(media)) { + return; // recoverable error. try again when connected again to the server... + } + break; + case UploadState.hasUploadToken: + if (!await handleUpload(media)) { + return; // recoverable error. try again when connected again to the server... + } + break; + case UploadState.isUploaded: + if (!await handleNotifyReceiver(media)) { + return; // recoverable error. try again when connected again to the server... + } + break; + case UploadState.receiverNotified: + return; } + } catch (e) { + // if the messageIds are already there notify the user about this error... + if (media.messageIds != null) { + for (int messageId in media.messageIds!) { + await twonlyDatabase.messagesDao.updateMessageByMessageId( + messageId, + MessagesCompanion( + errorWhileSending: Value(true), + ), + ); + } + } + await twonlyDatabase.mediaUploadsDao.deleteMediaUpload(mediaUploadId); + Logger("media_send.dart") + .shout("Non recoverable error while sending media file: $e"); + return; } - await twonlyDatabase.mediaUploadsDao.deleteMediaUpload(mediaUploadId); - Logger("media_send.dart") - .shout("Non recoverable error while sending media file: $e"); - } - // }); + }); + // this will be called until there is an recoverable error OR + // the upload is ready + await handleSingleMediaFile(mediaUploadId); } Future handleAddToMessageDb(MediaUpload media) async { @@ -137,6 +138,7 @@ Future handleAddToMessageDb(MediaUpload media) async { maxShowTime: media.metadata.maxShowTime, isRealTwonly: media.metadata.isRealTwonly, isVideo: media.metadata.isVideo, + mirrorVideo: media.metadata.mirrorVideo, ).toJson(), ), ), @@ -357,7 +359,11 @@ Future handleUpload(MediaUpload media) async { ), ); - await deleteMediaFile(media, "encrypted"); + try { + await deleteMediaFile(media, "encrypted"); + } catch (e) { + Logger("media_send.dart").shout("$e"); + } return true; } @@ -395,6 +401,7 @@ Future handleNotifyReceiver(MediaUpload media) async { maxShowTime: media.metadata.maxShowTime, isRealTwonly: media.metadata.isRealTwonly, isVideo: media.metadata.isVideo, + mirrorVideo: media.metadata.mirrorVideo, encryptionKey: media.encryptionData!.encryptionKey, encryptionMac: media.encryptionData!.encryptionMac, encryptionNonce: media.encryptionData!.encryptionNonce, diff --git a/lib/src/views/camera/camera_preview_view.dart b/lib/src/views/camera/camera_preview_view.dart index 9a59307..3f239b0 100644 --- a/lib/src/views/camera/camera_preview_view.dart +++ b/lib/src/views/camera/camera_preview_view.dart @@ -232,6 +232,7 @@ class _CameraPreviewViewState extends State { videoFilePath: videoFilePath, imageBytes: imageBytes, sendTo: widget.sendTo, + mirrorVideo: isFront && Platform.isAndroid, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 7da0beb..01bac3d 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -30,11 +30,17 @@ List removedLayers = []; const gMediaShowInfinite = 999999; class ShareImageEditorView extends StatefulWidget { - const ShareImageEditorView( - {super.key, this.imageBytes, this.sendTo, this.videoFilePath}); + const ShareImageEditorView({ + super.key, + this.imageBytes, + this.sendTo, + this.videoFilePath, + required this.mirrorVideo, + }); final Future? imageBytes; final XFile? videoFilePath; final Contact? sendTo; + final bool mirrorVideo; @override State createState() => _ShareImageEditorView(); } @@ -289,6 +295,7 @@ class _ShareImageEditorView extends State { maxShowTime: maxShowTime, preselectedUser: widget.sendTo, videoFilePath: widget.videoFilePath, + mirrorVideo: widget.mirrorVideo, ), ), ); @@ -357,6 +364,7 @@ class _ShareImageEditorView extends State { maxShowTime, widget.videoFilePath, videoWithAudio, + widget.mirrorVideo, ); if (context.mounted) { // ignore: use_build_context_synchronously @@ -405,7 +413,12 @@ class _ShareImageEditorView extends State { child: Stack( children: [ if (videoController != null) - Positioned.fill(child: VideoPlayer(videoController!)), + Positioned.fill( + child: Transform.flip( + flipX: widget.mirrorVideo, + child: VideoPlayer(videoController!), + ), + ), Screenshot( controller: screenshotController, child: LayersViewer( diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 7ed43ac..6f455fa 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -21,12 +21,14 @@ class ShareImageView extends StatefulWidget { {super.key, required this.imageBytesFuture, required this.isRealTwonly, + required this.mirrorVideo, required this.maxShowTime, this.preselectedUser, required this.videoFilePath, this.enableVideoAudio}); final Future imageBytesFuture; final bool isRealTwonly; + final bool mirrorVideo; final int maxShowTime; final XFile? videoFilePath; final Contact? preselectedUser; @@ -232,12 +234,14 @@ class _ShareImageView extends State { sendingImage = true; }); sendMediaFile( - _selectedUserIds.toList(), - imageBytes!, - widget.isRealTwonly, - widget.maxShowTime, - widget.videoFilePath, - widget.enableVideoAudio); + _selectedUserIds.toList(), + imageBytes!, + widget.isRealTwonly, + widget.maxShowTime, + widget.videoFilePath, + widget.enableVideoAudio, + widget.mirrorVideo, + ); if (context.mounted) { if (widget.preselectedUser != null) { Navigator.pop(context, true); diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index 3ead83c..308a186 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -47,6 +47,7 @@ class _MediaViewerViewState extends State { int maxShowTime = 999999; double progress = 0; bool isRealTwonly = false; + bool mirrorVideo = false; bool isDownloading = false; bool showSendTextMessageInput = false; @@ -120,6 +121,7 @@ class _MediaViewerViewState extends State { maxShowTime = 999999; imageSaving = false; imageSaved = false; + mirrorVideo = false; progress = 0; isDownloading = false; isRealTwonly = false; @@ -226,6 +228,7 @@ class _MediaViewerViewState extends State { setState(() { maxShowTime = content.maxShowTime; isDownloading = false; + mirrorVideo = content.mirrorVideo; }); } @@ -412,7 +415,12 @@ class _MediaViewerViewState extends State { child: Stack( children: [ if (videoController != null) - Positioned.fill(child: VideoPlayer(videoController!)), + Positioned.fill( + child: Transform.flip( + flipX: mirrorVideo, + child: VideoPlayer(videoController!), + ), + ), if (imageBytes != null) Positioned.fill( child: Image.memory(