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 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

View file

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

View file

@ -92,7 +92,10 @@ class MainCameraController {
}
final cameraControllerTemp = cameraController;
cameraController = null;
// 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();
}

View file

@ -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<Uint8List?> Function()? storeImageAsOriginal;
final Future<ScreenshotImage?> Function()? storeImageAsOriginal;
final bool displayButtonLabel;
final MediaFileService mediaService;
final bool isLoading;

View file

@ -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<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
final Future<Uint8List?>? mediaStoreFuture;
final Future<ScreenshotImage?>? mediaStoreFuture;
final MediaFileService mediaFileService;
final AdditionalMessageData? additionalData;
@ -46,7 +47,7 @@ class _ShareImageView extends State<ShareImageView> {
bool sendingImage = false;
bool mediaStoreFutureReady = false;
Uint8List? _imageBytes;
ScreenshotImage? _screenshotImage;
bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Group>> allGroupSub;
@ -69,7 +70,7 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> 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<ShareImageView> {
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<ShareImageView> {
),
child: ClipRRect(
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;
});
// 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<ShareImageView> {
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(

View file

@ -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<ShareImageEditorView> {
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<ShareImageEditorView> {
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<ShareImageEditorView> {
Future<ScreenshotImage?> 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<ShareImageEditorView> {
return image;
}
Future<Uint8List?> storeImageAsOriginal() async {
Future<ScreenshotImage?> storeImageAsOriginal() async {
if (mediaService.overlayImagePath.existsSync()) {
mediaService.overlayImagePath.deleteSync();
}
@ -477,11 +475,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
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 {
final image = await getEditedImageBytes();
Log.error('Could not load image bytes for gif!');
}
} else {
image = await getEditedImageBytes();
if (image == null) return null;
bytes = await image.getBytes();
if (bytes == null) {
@ -496,16 +499,38 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Log.error('MediaType not supported: ${media.type}');
}
}
return bytes;
return image;
}
Future<void> 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<void> storeIoImageAsDraft(ScreenshotImage screenshotImage) async {
final imageBytes = await screenshotImage.getBytes();
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 (!context.mounted) return;

View file

@ -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<bool> loader = Completer<bool>();
Future<void> load(dynamic image) async {
loader = Completer<bool>();
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;
}
}
}

View file

@ -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';
@ -16,23 +18,45 @@ class BackgroundLayer extends StatefulWidget {
class _BackgroundLayerState extends State<BackgroundLayer> {
@override
Widget build(BuildContext context) {
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) {
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
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(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton.filled(
Material(
elevation: 3,
shape: const CircleBorder(),
color: context.color.primary,
onPressed: () {
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
@ -326,11 +330,18 @@ class _ChatListViewState extends State<ChatListView> {
),
);
},
icon: FaIcon(
child: SizedBox(
width: 45,
height: 45,
child: Center(
child: FaIcon(
FontAwesomeIcons.qrcode,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
),
),
),
),
const SizedBox(height: 12),
FloatingActionButton(
backgroundColor: context.color.primary,

View file

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