This commit is contained in:
otsmr 2026-01-23 16:34:01 +01:00
parent 6720604fc3
commit 9813698e59
10 changed files with 134 additions and 85 deletions

View file

@ -7,6 +7,7 @@
- Adds support to switch between front and back cameras during video recording - Adds support to switch between front and back cameras during video recording
- Adds basic face filters - Adds basic face filters
- Improves image editor, like emojis or text under a drawing can be moved - 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 - Fixes issue with emojis disappearing in the image editor
## 0.0.86 ## 0.0.86

View file

@ -309,7 +309,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<bool> pushMediaEditor( Future<bool> pushMediaEditor(
ScreenshotImage? imageBytes, ScreenshotImage? screenshotImage,
File? videoFilePath, { File? videoFilePath, {
bool sharedFromGallery = false, bool sharedFromGallery = false,
MediaType? mediaType, MediaType? mediaType,
@ -345,7 +345,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
pageBuilder: (context, a1, a2) => ShareImageEditorView( pageBuilder: (context, a1, a2) => ShareImageEditorView(
imageBytesFuture: imageBytes, screenshotImage: screenshotImage,
sharedFromGallery: sharedFromGallery, sharedFromGallery: sharedFromGallery,
sendToGroup: widget.sendToGroup, sendToGroup: widget.sendToGroup,
mediaFileService: mediaFileService, mediaFileService: mediaFileService,

View file

@ -92,7 +92,10 @@ class MainCameraController {
} }
final cameraControllerTemp = cameraController; final cameraControllerTemp = cameraController;
cameraController = null; cameraController = null;
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
Future.delayed(const Duration(milliseconds: 100), () async {
await cameraControllerTemp?.dispose(); await cameraControllerTemp?.dispose();
});
initCameraStarted = false; initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
} }

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; 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/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/screenshot.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
const SaveToGalleryButton({ const SaveToGalleryButton({
@ -17,7 +17,7 @@ class SaveToGalleryButton extends StatefulWidget {
this.storeImageAsOriginal, this.storeImageAsOriginal,
super.key, super.key,
}); });
final Future<Uint8List?> Function()? storeImageAsOriginal; final Future<ScreenshotImage?> Function()? storeImageAsOriginal;
final bool displayButtonLabel; final bool displayButtonLabel;
final MediaFileService mediaService; final MediaFileService mediaService;
final bool isLoading; final bool isLoading;

View file

@ -2,7 +2,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.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/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.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_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/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
@ -30,7 +31,7 @@ class ShareImageView extends StatefulWidget {
}); });
final HashSet<String> selectedGroupIds; final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds; final void Function(String, bool) updateSelectedGroupIds;
final Future<Uint8List?>? mediaStoreFuture; final Future<ScreenshotImage?>? mediaStoreFuture;
final MediaFileService mediaFileService; final MediaFileService mediaFileService;
final AdditionalMessageData? additionalData; final AdditionalMessageData? additionalData;
@ -46,7 +47,7 @@ class _ShareImageView extends State<ShareImageView> {
bool sendingImage = false; bool sendingImage = false;
bool mediaStoreFutureReady = false; bool mediaStoreFutureReady = false;
Uint8List? _imageBytes; ScreenshotImage? _screenshotImage;
bool hideArchivedUsers = true; bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController(); final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Group>> allGroupSub; late StreamSubscription<List<Group>> allGroupSub;
@ -69,7 +70,7 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> initAsync() async { Future<void> initAsync() async {
if (widget.mediaStoreFuture != null) { if (widget.mediaStoreFuture != null) {
_imageBytes = await widget.mediaStoreFuture; _screenshotImage = await widget.mediaStoreFuture;
} }
mediaStoreFutureReady = true; mediaStoreFutureReady = true;
if (!mounted) return; if (!mounted) return;
@ -247,10 +248,11 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (widget.mediaFileService.mediaFile.type == MediaType.image && if (widget.mediaFileService.mediaFile.type == MediaType.image &&
_imageBytes != null && _screenshotImage?.image != null &&
gUser.showShowImagePreviewWhenSending) gUser.showShowImagePreviewWhenSending)
SizedBox( SizedBox(
height: 100, height: 100,
width: 100 * 9 / 16,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -261,7 +263,9 @@ class _ShareImageView extends State<ShareImageView> {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.memory(_imageBytes!), child: CustomPaint(
painter: UiImagePainter(_screenshotImage!.image!),
),
), ),
), ),
), ),
@ -286,6 +290,7 @@ class _ShareImageView extends State<ShareImageView> {
sendingImage = true; sendingImage = true;
}); });
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
await insertMediaFileInMessagesTable( await insertMediaFileInMessagesTable(
widget.mediaFileService, widget.mediaFileService,
widget.selectedGroupIds.toList(), widget.selectedGroupIds.toList(),
@ -294,12 +299,6 @@ class _ShareImageView extends State<ShareImageView> {
if (context.mounted) { if (context.mounted) {
Navigator.pop(context, true); Navigator.pop(context, true);
// if (widget.preselectedUser != null) {
// Navigator.pop(context, true);
// } else {
// Navigator.popUntil(context, (route) => route.isFirst, true);
// globalUpdateOfHomeViewPageIndex(1);
// }
} }
}, },
style: ButtonStyle( style: ButtonStyle(

View file

@ -39,13 +39,13 @@ class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({ const ShareImageEditorView({
required this.sharedFromGallery, required this.sharedFromGallery,
required this.mediaFileService, required this.mediaFileService,
this.screenshotImage,
this.previewLink, this.previewLink,
super.key, super.key,
this.imageBytesFuture,
this.sendToGroup, this.sendToGroup,
this.mainCameraController, this.mainCameraController,
}); });
final ScreenshotImage? imageBytesFuture; final ScreenshotImage? screenshotImage;
final Group? sendToGroup; final Group? sendToGroup;
final bool sharedFromGallery; final bool sharedFromGallery;
final MediaFileService mediaFileService; final MediaFileService mediaFileService;
@ -64,7 +64,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
double widthRatio = 1; double widthRatio = 1;
double heightRatio = 1; double heightRatio = 1;
double pixelRatio = 1; double pixelRatio = 1;
Uint8List? imageBytes;
VideoPlayerController? videoController; VideoPlayerController? videoController;
ImageItem currentImage = ImageItem(); ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
@ -93,8 +92,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (widget.mediaFileService.mediaFile.type == MediaType.image || if (widget.mediaFileService.mediaFile.type == MediaType.image ||
widget.mediaFileService.mediaFile.type == MediaType.gif) { widget.mediaFileService.mediaFile.type == MediaType.gif) {
if (widget.imageBytesFuture != null) { if (widget.screenshotImage != null) {
loadImage(widget.imageBytesFuture!); loadImage(widget.screenshotImage!);
} else { } else {
if (widget.mediaFileService.tempPath.existsSync()) { if (widget.mediaFileService.tempPath.existsSync()) {
loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath)); loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath));
@ -435,8 +434,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Future<ScreenshotImage?> getEditedImageBytes() async { Future<ScreenshotImage?> getEditedImageBytes() async {
if (layers.length == 1) { if (layers.length == 1) {
if (layers.first is BackgroundLayerData) { if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes; return (layers.first as BackgroundLayerData).image.image;
return ScreenshotImage(imageBytes: image);
} }
} }
@ -465,7 +463,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return image; return image;
} }
Future<Uint8List?> storeImageAsOriginal() async { Future<ScreenshotImage?> storeImageAsOriginal() async {
if (mediaService.overlayImagePath.existsSync()) { if (mediaService.overlayImagePath.existsSync()) {
mediaService.overlayImagePath.deleteSync(); mediaService.overlayImagePath.deleteSync();
} }
@ -477,11 +475,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mediaService.originalPath.deleteSync(); mediaService.originalPath.deleteSync();
} }
} }
var bytes = imageBytes; ScreenshotImage? image;
var bytes = await widget.screenshotImage?.getBytes();
if (media.type == MediaType.gif) { if (media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); if (bytes != null) {
mediaService.originalPath.writeAsBytesSync(bytes.toList());
} else { } else {
final image = await getEditedImageBytes(); Log.error('Could not load image bytes for gif!');
}
} else {
image = await getEditedImageBytes();
if (image == null) return null; if (image == null) return null;
bytes = await image.getBytes(); bytes = await image.getBytes();
if (bytes == null) { if (bytes == null) {
@ -496,16 +499,38 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Log.error('MediaType not supported: ${media.type}'); Log.error('MediaType not supported: ${media.type}');
} }
} }
return image;
return bytes;
} }
Future<void> loadImage(ScreenshotImage imageBytesFuture) async { Future<void> storeIoImageAsDraft(ScreenshotImage screenshotImage) async {
imageBytes = await imageBytesFuture.getBytes(); final imageBytes = await screenshotImage.getBytes();
// store this image so it can be used as a draft in case the app is restarted
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
}
Future<void> 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 (isDisposed) return;
if (!context.mounted) return; if (!context.mounted) return;

View file

@ -1,36 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'package:twonly/src/utils/screenshot.dart';
import 'package:flutter/material.dart';
class ImageItem { class ImageItem {
ImageItem([dynamic image]) { ImageItem();
if (image != null) unawaited(load(image));
}
int width = 1; int width = 1;
int height = 1; int height = 1;
Uint8List bytes = Uint8List.fromList([]); ScreenshotImage? image;
Completer<bool> loader = Completer<bool>(); Completer<bool> loader = Completer<bool>();
Future<void> load(dynamic image) async { void load(ScreenshotImage img) {
loader = Completer<bool>(); image = img;
if (image?.image != null) {
if (image is ImageItem) { height = image!.image!.height;
bytes = image.bytes; width = image!.image!.width;
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);
} }
} }
} }

View file

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
@ -16,23 +18,45 @@ class BackgroundLayer extends StatefulWidget {
class _BackgroundLayerState extends State<BackgroundLayer> { class _BackgroundLayerState extends State<BackgroundLayer> {
@override @override
Widget build(BuildContext context) { void initState() {
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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.layerData.imageLoaded = true; widget.layerData.imageLoaded = true;
}); });
super.initState();
} }
return child;
}, @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(),
padding: EdgeInsets.zero,
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;
}
}

View file

@ -314,9 +314,13 @@ class _ChatListViewState extends State<ChatListView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
IconButton.filled( Material(
elevation: 3,
shape: const CircleBorder(),
color: context.color.primary, color: context.color.primary,
onPressed: () { child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -326,11 +330,18 @@ class _ChatListViewState extends State<ChatListView> {
), ),
); );
}, },
icon: FaIcon( child: SizedBox(
width: 45,
height: 45,
child: Center(
child: FaIcon(
FontAwesomeIcons.qrcode, FontAwesomeIcons.qrcode,
color: isDarkMode(context) ? Colors.black : Colors.white, color: isDarkMode(context) ? Colors.black : Colors.white,
), ),
), ),
),
),
),
const SizedBox(height: 12), const SizedBox(height: 12),
FloatingActionButton( FloatingActionButton(
backgroundColor: context.color.primary, backgroundColor: context.color.primary,

View file

@ -105,7 +105,11 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
return; return;
} }
if (orgMediaService.storedPath.existsSync()) {
orgMediaService.storedPath.copySync(newMediaService.originalPath.path); orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
} else if (orgMediaService.tempPath.existsSync()) {
orgMediaService.tempPath.copySync(newMediaService.originalPath.path);
}
if (!mounted) return; if (!mounted) return;