diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cefb55..27eaefd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ ## 0.1.3 -- New: Developer settings to reduce flames +- New: Video stabilization +- New: Crop or rotate images before share them - New: Clicking on “Text Notifications” will now open the chat directly (Android only) +- New: Developer settings to reduce flames - Improve: Improved troubleshooting for issues with push notifications - Fix: Flash not activated when starting a video recording - Fix: Problem sending media when a recipient has deleted their account. diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 286fc68..a581990 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -26,19 +26,19 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { Log.error('Did not update reaction as it is not an emoji!'); return; } - final msg = - await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + final msg = await twonlyDB.messagesDao + .getMessageById(messageId) + .getSingleOrNull(); if (msg == null || msg.groupId != groupId) return; try { if (remove) { - await (delete(reactions) - ..where( - (t) => - t.senderId.equals(contactId) & - t.messageId.equals(messageId) & - t.emoji.equals(emoji), - )) + await (delete(reactions)..where( + (t) => + t.senderId.equals(contactId) & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) .go(); } else { await into(reactions).insertOnConflictUpdate( @@ -63,18 +63,18 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { Log.error('Did not update reaction as it is not an emoji!'); return; } - final msg = - await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + final msg = await twonlyDB.messagesDao + .getMessageById(messageId) + .getSingleOrNull(); if (msg == null) return; try { - await (delete(reactions) - ..where( - (t) => - t.senderId.isNull() & - t.messageId.equals(messageId) & - t.emoji.equals(emoji), - )) + await (delete(reactions)..where( + (t) => + t.senderId.isNull() & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) .go(); if (!remove) { await into(reactions).insert( @@ -98,20 +98,19 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { } Stream watchLastReactions(String groupId) { - final query = (select(reactions) - ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) - .join( - [ - innerJoin( - messages, - messages.messageId.equalsExp(reactions.messageId), - useColumns: false, - ), - ], - ) - ..where(messages.groupId.equals(groupId)) - // ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) - ..limit(1); + final query = + (select(reactions)).join( + [ + innerJoin( + messages, + messages.messageId.equalsExp(reactions.messageId), + useColumns: false, + ), + ], + ) + ..where(messages.groupId.equals(groupId)) + ..orderBy([OrderingTerm.desc(messages.createdAt)]) + ..limit(1); return query.map((row) => row.readTable(reactions)).watchSingleOrNull(); } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 8f30a81..40dccb3 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -53,8 +53,8 @@ class UserData { @JsonKey(defaultValue: false) bool requestedAudioPermission = false; - @JsonKey(defaultValue: false) - bool videoStabilizationEnabled = false; + @JsonKey(defaultValue: true) + bool videoStabilizationEnabled = true; @JsonKey(defaultValue: true) bool showFeedbackShortcut = true; diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 4fb9b16..2cc6357 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -4,6 +4,7 @@ import 'package:camera/camera.dart'; import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart' show Value; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; @@ -133,7 +134,7 @@ class MainCameraController { await cameraController?.initialize(); await cameraController?.startImageStream(_processCameraImage); await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); - if (gUser.videoStabilizationEnabled) { + if (gUser.videoStabilizationEnabled && !kDebugMode) { await cameraController?.setVideoStabilizationMode( VideoStabilizationMode.level1, ); diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index 4e31b88..162901f 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -109,13 +109,21 @@ class _ShareImageEditorView extends State { sendingOrLoadingImage = false; loadingImage = false; }); - videoController = VideoPlayerController.file(mediaService.originalPath); + videoController = VideoPlayerController.file( + mediaService.originalPath, + videoPlayerOptions: VideoPlayerOptions( + mixWithOthers: true, + ), + ); videoController?.setLooping(true); - videoController?.initialize().then((_) async { - await videoController!.play(); - setState(() {}); - // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler - }).catchError(Log.error); + videoController + ?.initialize() + .then((_) async { + await videoController!.play(); + setState(() {}); + }) + // ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error + .catchError(Log.error); } } @@ -205,8 +213,8 @@ class _ShareImageEditorView extends State { List get actionsAtTheRight { if (layers.isNotEmpty && - layers.last.isEditing && - layers.last.hasCustomActionButtons) { + (layers.first.isEditing || + (layers.last.isEditing && layers.last.hasCustomActionButtons))) { return []; } return [ @@ -246,13 +254,15 @@ class _ShareImageEditorView extends State { Icons.add_reaction_outlined, tooltipText: context.lang.addEmoji, onPressed: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (context) { - return const EmojiPickerBottom(); - }, - ) as Layer?; + final layer = + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (context) { + return const EmojiPickerBottom(); + }, + ) + as Layer?; if (layer == null) return; undoLayers.clear(); removedLayers.clear(); @@ -265,19 +275,20 @@ class _ShareImageEditorView extends State { count: (media.type == MediaType.video) ? '0' : media.displayLimitInMilliseconds == null - ? '∞' - : (media.displayLimitInMilliseconds! ~/ 1000).toString(), + ? '∞' + : (media.displayLimitInMilliseconds! ~/ 1000).toString(), child: ActionButton( (media.type == MediaType.video) ? media.displayLimitInMilliseconds == null - ? Icons.repeat_rounded - : Icons.repeat_one_rounded + ? Icons.repeat_rounded + : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, onPressed: _setImageDisplayTime, ), ), - if (media.type == MediaType.video) + if (media.type == MediaType.video) ...[ + const SizedBox(height: 8), ActionButton( (mediaService.removeAudio) ? Icons.volume_off_rounded @@ -296,6 +307,29 @@ class _ShareImageEditorView extends State { if (mounted) setState(() {}); }, ), + ], + if (media.type == MediaType.image) ...[ + const SizedBox(height: 8), + ActionButton( + Icons.crop_rotate_outlined, + tooltipText: 'Crop or rotate image', + color: Colors.white, + onPressed: () async { + final first = layers.first; + if (first is BackgroundLayerData) { + first.isEditing = !first.isEditing; + } + setState(() {}); + // await mediaService.toggleRemoveAudio(); + // if (mediaService.removeAudio) { + // await videoController?.setVolume(0); + // } else { + // await videoController?.setVolume(100); + // } + // if (mounted) setState(() {}); + }, + ), + ], const SizedBox(height: 8), ActionButton( FontAwesomeIcons.shieldHeart, @@ -348,8 +382,8 @@ class _ShareImageEditorView extends State { List get actionsAtTheTop { if (layers.isNotEmpty && - layers.last.isEditing && - layers.last.hasCustomActionButtons) { + (layers.first.isEditing || + (layers.last.isEditing && layers.last.hasCustomActionButtons))) { return []; } return [ @@ -411,18 +445,20 @@ class _ShareImageEditorView extends State { await videoController?.pause(); if (isDisposed || !mounted) return; - final wasSend = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageView( - selectedGroupIds: selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - mediaStoreFuture: mediaStoreFuture, - mediaFileService: mediaService, - additionalData: getAdditionalData(), - ), - ), - ) as bool?; + final wasSend = + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageView( + selectedGroupIds: selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + mediaStoreFuture: mediaStoreFuture, + mediaFileService: mediaService, + additionalData: getAdditionalData(), + ), + ), + ) + as bool?; if (wasSend != null && wasSend && mounted) { widget.mainCameraController?.onImageSend(); Navigator.pop(context, true); @@ -552,8 +588,9 @@ class _ShareImageEditorView extends State { }); // It is important that the user can sending the image only when the image is fully loaded otherwise if the user // will click on send before the image is painted the screenshot will be transparent.. - _imageLoadingTimer = - Timer.periodic(const Duration(milliseconds: 10), (timer) { + _imageLoadingTimer = Timer.periodic(const Duration(milliseconds: 10), ( + timer, + ) { final imageLayer = layers.first; if (imageLayer is BackgroundLayerData) { if (imageLayer.imageLoaded) { @@ -619,8 +656,9 @@ class _ShareImageEditorView extends State { await askToCloseThenClose(); }, child: Scaffold( - backgroundColor: - widget.sharedFromGallery ? null : Colors.white.withAlpha(0), + backgroundColor: widget.sharedFromGallery + ? null + : Colors.white.withAlpha(0), resizeToAvoidBottomInset: false, body: Stack( fit: StackFit.expand, @@ -667,8 +705,9 @@ class _ShareImageEditorView extends State { OutlinedButton( style: OutlinedButton.styleFrom( iconColor: Theme.of(context).colorScheme.primary, - foregroundColor: - Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of( + context, + ).colorScheme.primary, ), onPressed: pushShareImageView, child: const FaIcon(FontAwesomeIcons.userPlus), @@ -681,9 +720,9 @@ class _ShareImageEditorView extends State { width: 12, child: CircularProgressIndicator( strokeWidth: 2, - color: Theme.of(context) - .colorScheme - .inversePrimary, + color: Theme.of( + context, + ).colorScheme.inversePrimary, ), ) : const FaIcon(FontAwesomeIcons.solidPaperPlane), diff --git a/lib/src/views/camera/share_image_editor/layers/background.layer.dart b/lib/src/views/camera/share_image_editor/layers/background.layer.dart index ab0408f..789f716 100755 --- a/lib/src/views/camera/share_image_editor/layers/background.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/background.layer.dart @@ -1,6 +1,10 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; class BackgroundLayer extends StatefulWidget { @@ -29,14 +33,47 @@ class _BackgroundLayerState extends State { Widget build(BuildContext context) { final scImage = widget.layerData.image.image; if (scImage == null || scImage.image == null) return Container(); - return Container( - width: widget.layerData.image.width.toDouble(), - height: widget.layerData.image.height.toDouble(), - padding: EdgeInsets.zero, - color: Colors.transparent, - child: CustomPaint( - painter: UiImagePainter(scImage.image!), - ), + return Stack( + children: [ + Positioned.fill( + child: PhotoView.customChild( + enableRotation: true, + initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + child: Container( + width: widget.layerData.image.width.toDouble(), + height: widget.layerData.image.height.toDouble(), + padding: EdgeInsets.zero, + color: Colors.transparent, + child: CustomPaint( + painter: UiImagePainter(scImage.image!), + ), + ), + ), + ), + if (widget.layerData.isEditing) + Positioned( + top: 5, + left: 5, + right: 50, + child: Row( + children: [ + ActionButton( + FontAwesomeIcons.check, + tooltipText: context.lang.imageEditorDrawOk, + onPressed: () async { + widget.layerData.isEditing = false; + widget.onUpdate!(); + setState(() {}); + }, + ), + ], + ), + ), + ], ); } } diff --git a/lib/src/views/camera/share_image_editor/layers_viewer.dart b/lib/src/views/camera/share_image_editor/layers_viewer.dart index e53e3a8..390dcab 100644 --- a/lib/src/views/camera/share_image_editor/layers_viewer.dart +++ b/lib/src/views/camera/share_image_editor/layers_viewer.dart @@ -23,11 +23,15 @@ class LayersViewer extends StatelessWidget { alignment: Alignment.center, children: [ ...layers.whereType().map((layerItem) { - return BackgroundLayer( - key: layerItem.key, - layerData: layerItem, - onUpdate: onUpdate, - ); + if (!layerItem.isEditing) { + return BackgroundLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); + } else { + return Container(); + } }), ...layers.whereType().map((layerItem) { return FilterLayer( @@ -37,39 +41,50 @@ class LayersViewer extends StatelessWidget { }), ...layers .where( - (layerItem) => - layerItem is EmojiLayerData || - layerItem is DrawLayerData || - layerItem is LinkPreviewLayerData || - layerItem is TextLayerData, - ) + (layerItem) => + layerItem is EmojiLayerData || + layerItem is DrawLayerData || + layerItem is LinkPreviewLayerData || + layerItem is TextLayerData, + ) .map((layerItem) { - if (layerItem is EmojiLayerData) { - return EmojiLayer( - key: layerItem.key, - layerData: layerItem, - onUpdate: onUpdate, - ); - } else if (layerItem is DrawLayerData) { - return DrawLayer( - key: layerItem.key, - layerData: layerItem, - onUpdate: onUpdate, - ); - } else if (layerItem is TextLayerData) { - return TextLayer( - key: layerItem.key, - layerData: layerItem, - onUpdate: onUpdate, - ); - } else if (layerItem is LinkPreviewLayerData) { - return LinkPreviewLayer( + if (layerItem is EmojiLayerData) { + return EmojiLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); + } else if (layerItem is DrawLayerData) { + return DrawLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); + } else if (layerItem is TextLayerData) { + return TextLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); + } else if (layerItem is LinkPreviewLayerData) { + return LinkPreviewLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); + } + return Container(); + }), + ...layers.whereType().map((layerItem) { + if (layerItem.isEditing) { + return BackgroundLayer( key: layerItem.key, layerData: layerItem, onUpdate: onUpdate, ); + } else { + return Container(); } - return Container(); }), ], ); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 43696bd..863b058 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -75,9 +75,6 @@ class _UserListItem extends State { setState(() { _lastReaction = update; }); - // protectUpdateState.protect(() async { - // await updateState(lastMessage, update, messagesNotOpened); - // }); }); _messagesNotOpenedStream = twonlyDB.messagesDao diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 525836d..fec0a32 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -287,7 +287,15 @@ class _MediaViewerViewState extends State { var timerRequired = false; if (currentMediaLocal.mediaFile.type == MediaType.video) { - videoController = VideoPlayerController.file(currentMediaLocal.tempPath); + videoController = VideoPlayerController.file( + currentMediaLocal.tempPath, + videoPlayerOptions: VideoPlayerOptions( + // only mix in case the video can be played multiple times, + // otherwise stop the background music in case the video contains audio + mixWithOthers: + currentMediaLocal.mediaFile.displayLimitInMilliseconds == null, + ), + ); await videoController?.setLooping( currentMediaLocal.mediaFile.displayLimitInMilliseconds == null, ); diff --git a/lib/src/views/components/video_player_wrapper.dart b/lib/src/views/components/video_player_wrapper.dart index 80833d0..be470fc 100644 --- a/lib/src/views/components/video_player_wrapper.dart +++ b/lib/src/views/components/video_player_wrapper.dart @@ -21,7 +21,12 @@ class _VideoPlayerWrapperState extends State { @override void initState() { super.initState(); - _controller = VideoPlayerController.file(widget.videoPath); + _controller = VideoPlayerController.file( + widget.videoPath, + videoPlayerOptions: VideoPlayerOptions( + mixWithOthers: true, + ), + ); unawaited( _controller.initialize().then((_) async {