From 9813698e59866357766c559e27383289529bdae5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 23 Jan 2026 16:34:01 +0100 Subject: [PATCH] fix #380 --- CHANGELOG.md | 1 + .../camera_preview_controller_view.dart | 4 +- .../main_camera_controller.dart | 5 +- .../save_to_gallery.dart | 4 +- .../share_image_contact_selection.view.dart | 23 ++++---- .../views/camera/share_image_editor.view.dart | 59 +++++++++++++------ .../camera/share_image_editor/image_item.dart | 34 +++-------- .../layers/background.layer.dart | 46 +++++++++++---- lib/src/views/chats/chat_list.view.dart | 37 ++++++++---- .../memories/memories_photo_slider.view.dart | 6 +- 10 files changed, 134 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbdec6..5aae546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Adds support to switch between front and back cameras during video recording - Adds basic face filters - Improves image editor, like emojis or text under a drawing can be moved +- Improves speed after taking a picture - Fixes issue with emojis disappearing in the image editor ## 0.0.86 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index c3064ee..34e1e78 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -309,7 +309,7 @@ class _CameraPreviewViewState extends State { } Future pushMediaEditor( - ScreenshotImage? imageBytes, + ScreenshotImage? screenshotImage, File? videoFilePath, { bool sharedFromGallery = false, MediaType? mediaType, @@ -345,7 +345,7 @@ class _CameraPreviewViewState extends State { PageRouteBuilder( opaque: false, pageBuilder: (context, a1, a2) => ShareImageEditorView( - imageBytesFuture: imageBytes, + screenshotImage: screenshotImage, sharedFromGallery: sharedFromGallery, sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, 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 09675cd..a6de9e9 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 @@ -92,7 +92,10 @@ class MainCameraController { } final cameraControllerTemp = cameraController; cameraController = null; - await cameraControllerTemp?.dispose(); + // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) + Future.delayed(const Duration(milliseconds: 100), () async { + await cameraControllerTemp?.dispose(); + }); initCameraStarted = false; selectedCameraDetails = SelectedCameraDetails(); } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 85c57d4..282ccc5 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; @@ -8,6 +7,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/screenshot.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ @@ -17,7 +17,7 @@ class SaveToGalleryButton extends StatefulWidget { this.storeImageAsOriginal, super.key, }); - final Future Function()? storeImageAsOriginal; + final Future Function()? storeImageAsOriginal; final bool displayButtonLabel; final MediaFileService mediaService; final bool isLoading; diff --git a/lib/src/views/camera/share_image_contact_selection.view.dart b/lib/src/views/camera/share_image_contact_selection.view.dart index 7cdf082..b93fadb 100644 --- a/lib/src/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/views/camera/share_image_contact_selection.view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; @@ -14,7 +13,9 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/share_image_contact_selection/best_friends_selector.dart'; +import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; @@ -30,7 +31,7 @@ class ShareImageView extends StatefulWidget { }); final HashSet selectedGroupIds; final void Function(String, bool) updateSelectedGroupIds; - final Future? mediaStoreFuture; + final Future? mediaStoreFuture; final MediaFileService mediaFileService; final AdditionalMessageData? additionalData; @@ -46,7 +47,7 @@ class _ShareImageView extends State { bool sendingImage = false; bool mediaStoreFutureReady = false; - Uint8List? _imageBytes; + ScreenshotImage? _screenshotImage; bool hideArchivedUsers = true; final TextEditingController searchUserName = TextEditingController(); late StreamSubscription> allGroupSub; @@ -69,7 +70,7 @@ class _ShareImageView extends State { Future initAsync() async { if (widget.mediaStoreFuture != null) { - _imageBytes = await widget.mediaStoreFuture; + _screenshotImage = await widget.mediaStoreFuture; } mediaStoreFutureReady = true; if (!mounted) return; @@ -247,10 +248,11 @@ class _ShareImageView extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ if (widget.mediaFileService.mediaFile.type == MediaType.image && - _imageBytes != null && + _screenshotImage?.image != null && gUser.showShowImagePreviewWhenSending) SizedBox( height: 100, + width: 100 * 9 / 16, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -261,7 +263,9 @@ class _ShareImageView extends State { ), child: ClipRRect( borderRadius: BorderRadius.circular(12), - child: Image.memory(_imageBytes!), + child: CustomPaint( + painter: UiImagePainter(_screenshotImage!.image!), + ), ), ), ), @@ -286,6 +290,7 @@ class _ShareImageView extends State { sendingImage = true; }); + // in case mediaStoreFutureReady is ready, the image is stored in the originalPath await insertMediaFileInMessagesTable( widget.mediaFileService, widget.selectedGroupIds.toList(), @@ -294,12 +299,6 @@ class _ShareImageView extends State { if (context.mounted) { Navigator.pop(context, true); - // if (widget.preselectedUser != null) { - // Navigator.pop(context, true); - // } else { - // Navigator.popUntil(context, (route) => route.isFirst, true); - // globalUpdateOfHomeViewPageIndex(1); - // } } }, style: ButtonStyle( diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index a27f826..387e749 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -39,13 +39,13 @@ class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ required this.sharedFromGallery, required this.mediaFileService, + this.screenshotImage, this.previewLink, super.key, - this.imageBytesFuture, this.sendToGroup, this.mainCameraController, }); - final ScreenshotImage? imageBytesFuture; + final ScreenshotImage? screenshotImage; final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; @@ -64,7 +64,6 @@ class _ShareImageEditorView extends State { double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; - Uint8List? imageBytes; VideoPlayerController? videoController; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -93,8 +92,8 @@ class _ShareImageEditorView extends State { if (widget.mediaFileService.mediaFile.type == MediaType.image || widget.mediaFileService.mediaFile.type == MediaType.gif) { - if (widget.imageBytesFuture != null) { - loadImage(widget.imageBytesFuture!); + if (widget.screenshotImage != null) { + loadImage(widget.screenshotImage!); } else { if (widget.mediaFileService.tempPath.existsSync()) { loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath)); @@ -435,8 +434,7 @@ class _ShareImageEditorView extends State { Future getEditedImageBytes() async { if (layers.length == 1) { if (layers.first is BackgroundLayerData) { - final image = (layers.first as BackgroundLayerData).image.bytes; - return ScreenshotImage(imageBytes: image); + return (layers.first as BackgroundLayerData).image.image; } } @@ -465,7 +463,7 @@ class _ShareImageEditorView extends State { return image; } - Future storeImageAsOriginal() async { + Future storeImageAsOriginal() async { if (mediaService.overlayImagePath.existsSync()) { mediaService.overlayImagePath.deleteSync(); } @@ -477,11 +475,16 @@ class _ShareImageEditorView extends State { mediaService.originalPath.deleteSync(); } } - var bytes = imageBytes; + ScreenshotImage? image; + var bytes = await widget.screenshotImage?.getBytes(); if (media.type == MediaType.gif) { - mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + if (bytes != null) { + mediaService.originalPath.writeAsBytesSync(bytes.toList()); + } else { + Log.error('Could not load image bytes for gif!'); + } } else { - final image = await getEditedImageBytes(); + image = await getEditedImageBytes(); if (image == null) return null; bytes = await image.getBytes(); if (bytes == null) { @@ -496,16 +499,38 @@ class _ShareImageEditorView extends State { Log.error('MediaType not supported: ${media.type}'); } } - - return bytes; + return image; } - Future loadImage(ScreenshotImage imageBytesFuture) async { - imageBytes = await imageBytesFuture.getBytes(); - // store this image so it can be used as a draft in case the app is restarted + Future storeIoImageAsDraft(ScreenshotImage screenshotImage) async { + final imageBytes = await screenshotImage.getBytes(); mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + } + + Future loadImage(ScreenshotImage screenshotImage) async { + if (screenshotImage.image == null && + screenshotImage.imageBytes == null && + screenshotImage.imageBytesFuture != null) { + // this ensures that the imageBytes are defined + await storeIoImageAsDraft(screenshotImage); + } else { + // store this image so it can be used as a draft in case the app is restarted + unawaited(storeIoImageAsDraft(screenshotImage)); + } + + if (screenshotImage.image == null) { + final imageBytes = await screenshotImage.getBytes(); + if (imageBytes != null) { + screenshotImage.image = await decodeImageFromList(imageBytes); + } + } + if (screenshotImage.image == null) { + Log.error('Could not load screenshotImage.image'); + return; + } + + currentImage.load(screenshotImage); - await currentImage.load(imageBytes); if (isDisposed) return; if (!context.mounted) return; diff --git a/lib/src/views/camera/share_image_editor/image_item.dart b/lib/src/views/camera/share_image_editor/image_item.dart index 6468ca7..c2511d5 100755 --- a/lib/src/views/camera/share_image_editor/image_item.dart +++ b/lib/src/views/camera/share_image_editor/image_item.dart @@ -1,36 +1,18 @@ import 'dart:async'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/screenshot.dart'; class ImageItem { - ImageItem([dynamic image]) { - if (image != null) unawaited(load(image)); - } + ImageItem(); int width = 1; int height = 1; - Uint8List bytes = Uint8List.fromList([]); + ScreenshotImage? image; Completer loader = Completer(); - Future load(dynamic image) async { - loader = Completer(); - - if (image is ImageItem) { - bytes = image.bytes; - - height = image.height; - width = image.width; - - return loader.complete(true); - } else if (image is Uint8List) { - bytes = image; - final decodedImage = await decodeImageFromList(bytes); - - height = decodedImage.height; - width = decodedImage.width; - - return loader.complete(true); - } else { - return loader.complete(false); + void load(ScreenshotImage img) { + image = img; + if (image?.image != null) { + height = image!.image!.height; + width = image!.image!.width; } } } 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 f12816c..ecd3ab7 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,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; @@ -15,24 +17,46 @@ class BackgroundLayer extends StatefulWidget { } class _BackgroundLayerState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.layerData.imageLoaded = true; + }); + super.initState(); + } + @override 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(), - // color: Theme.of(context).colorScheme.surface, padding: EdgeInsets.zero, - child: Image.memory( - widget.layerData.image.bytes, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded || frame != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.layerData.imageLoaded = true; - }); - } - return child; - }, + color: Colors.green, + child: CustomPaint( + painter: UiImagePainter(scImage.image!), ), ); } } + +class UiImagePainter extends CustomPainter { + UiImagePainter(this.image); + final ui.Image image; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawImageRect( + image, + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), + Rect.fromLTWH(0, 0, size.width, size.height), + Paint(), + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 9bcf0ed..e15f2d0 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -314,21 +314,32 @@ class _ChatListViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - IconButton.filled( + Material( + elevation: 3, + shape: const CircleBorder(), color: context.color.primary, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PublicProfileView(); - }, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PublicProfileView(); + }, + ), + ); + }, + child: SizedBox( + width: 45, + height: 45, + child: Center( + child: FaIcon( + FontAwesomeIcons.qrcode, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), ), - ); - }, - icon: FaIcon( - FontAwesomeIcons.qrcode, - color: isDarkMode(context) ? Colors.black : Colors.white, + ), ), ), const SizedBox(height: 12), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 2b1c9b4..65c16ce 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -105,7 +105,11 @@ class _MemoriesPhotoSliderViewState extends State { return; } - orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + if (orgMediaService.storedPath.existsSync()) { + orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + } else if (orgMediaService.tempPath.existsSync()) { + orgMediaService.tempPath.copySync(newMediaService.originalPath.path); + } if (!mounted) return;