mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 17:06:47 +00:00
fix #380
This commit is contained in:
parent
6720604fc3
commit
9813698e59
10 changed files with 134 additions and 85 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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<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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BackgroundLayer> {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,21 +314,32 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -105,7 +105,11 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue