make images visible before sending #356 and remove dependencies #333

This commit is contained in:
otsmr 2025-12-26 21:10:32 +01:00
parent 27483bccd6
commit 910f5f79fa
22 changed files with 244 additions and 64 deletions

@ -1 +1 @@
Subproject commit fb66274bf729cde6f7184ec6f7f9ea89f12450fd
Subproject commit 83475a912851acb6a718ea32a6f0f754d64a50d8

View file

@ -457,5 +457,6 @@
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
"startWithCameraOpen": "Mit geöffneter Kamera starten"
"startWithCameraOpen": "Mit geöffneter Kamera starten",
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen"
}

View file

@ -487,5 +487,6 @@
"gotLinkFromFriend": "Yes, I got the link from my friend!",
"couldNotVerifyUsername": "Could not verify {username}",
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
"startWithCameraOpen": "Start with camera open"
"startWithCameraOpen": "Start with camera open",
"showImagePreviewWhenSending": "Display image preview when selecting recipients"
}

View file

@ -2845,6 +2845,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Start with camera open'**
String get startWithCameraOpen;
/// No description provided for @showImagePreviewWhenSending.
///
/// In en, this message translates to:
/// **'Display image preview when selecting recipients'**
String get showImagePreviewWhenSending;
}
class _AppLocalizationsDelegate

View file

@ -1573,4 +1573,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get startWithCameraOpen => 'Mit geöffneter Kamera starten';
@override
String get showImagePreviewWhenSending =>
'Bildvorschau bei der Auswahl von Empfängern anzeigen';
}

View file

@ -1563,4 +1563,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get startWithCameraOpen => 'Start with camera open';
@override
String get showImagePreviewWhenSending =>
'Display image preview when selecting recipients';
}

View file

@ -59,6 +59,9 @@ class UserData {
@JsonKey(defaultValue: true)
bool showFeedbackShortcut = true;
@JsonKey(defaultValue: true)
bool showShowImagePreviewWhenSending = true;
@JsonKey(defaultValue: true)
bool startWithCameraOpen = true;

View file

@ -32,6 +32,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? true
..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String)
@ -94,6 +96,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut,
'showShowImagePreviewWhenSending':
instance.showShowImagePreviewWhenSending,
'startWithCameraOpen': instance.startWithCameraOpen,
'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions,

View file

@ -87,6 +87,7 @@ Future<MediaFileService?> initializeMediaUpload(
Future<void> insertMediaFileInMessagesTable(
MediaFileService mediaService,
List<String> groupIds,
Future<Uint8List?>? imageStoreAwait,
) async {
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
@ -117,6 +118,13 @@ Future<void> insertMediaFileInMessagesTable(
}
}
if (imageStoreAwait != null) {
if (await imageStoreAwait == null) {
Log.error('image store as original did return false...');
return;
}
}
unawaited(startBackgroundMediaUpload(mediaService));
}

View file

@ -0,0 +1,104 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as io;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:twonly/src/utils/log.dart';
class ScreenshotImage {
ScreenshotImage({
this.image,
this.imageBytes,
this.imageBytesFuture,
this.file,
});
io.Image? image;
Uint8List? imageBytes;
Future<Uint8List>? imageBytesFuture;
File? file;
Future<Uint8List?> getBytes() async {
if (imageBytes != null) {
return imageBytes;
}
if (imageBytesFuture != null) {
return imageBytesFuture;
}
if (file != null) {
return file!.readAsBytes();
}
if (image == null) return null;
final img = await image!.toByteData(format: io.ImageByteFormat.png);
if (img == null) {
Log.error('Got no image');
return null;
}
return imageBytes = img.buffer.asUint8List();
}
}
class ScreenshotController {
ScreenshotController() {
_containerKey = GlobalKey();
}
late GlobalKey _containerKey;
Future<ScreenshotImage?> capture({double? pixelRatio}) async {
try {
final findRenderObject = _containerKey.currentContext?.findRenderObject();
if (findRenderObject == null) {
return null;
}
final boundary = findRenderObject as RenderRepaintBoundary;
final context = _containerKey.currentContext;
var tmpPixelRatio = pixelRatio;
if (tmpPixelRatio == null) {
if (context != null && context.mounted) {
tmpPixelRatio =
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
}
}
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
return ScreenshotImage(image: image);
} catch (e) {
Log.error(e);
}
return null;
}
}
class Screenshot extends StatefulWidget {
const Screenshot({
required this.child,
required this.controller,
super.key,
});
final Widget? child;
final ScreenshotController controller;
@override
State<Screenshot> createState() {
return ScreenshotState();
}
}
class ScreenshotState extends State<Screenshot> with TickerProviderStateMixin {
late ScreenshotController _controller;
@override
void initState() {
super.initState();
_controller = widget.controller;
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _controller._containerKey,
child: widget.child,
);
}
}

View file

@ -1,6 +1,6 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
@ -19,6 +18,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
@ -316,7 +316,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> takePicture() async {
if (_sharePreviewIsShown || _isVideoRecording) return;
late Future<Uint8List?> imageBytes;
setState(() {
_sharePreviewIsShown = true;
@ -345,10 +344,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return;
}
imageBytes = mc.screenshotController
final image = await mc.screenshotController
.capture(pixelRatio: MediaQuery.of(context).devicePixelRatio);
if (await pushMediaEditor(imageBytes, null)) {
if (await pushMediaEditor(image, null)) {
return;
}
setState(() {
@ -357,7 +356,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
Future<bool> pushMediaEditor(
Future<Uint8List?>? imageBytes,
ScreenshotImage? imageBytes,
File? videoFilePath, {
bool sharedFromGallery = false,
MediaType? mediaType,
@ -478,7 +477,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Log.info('Picket from gallery: ${pickedFile.path}');
File? videoFilePath;
Future<Uint8List>? imageBytes;
ScreenshotImage? image;
MediaType? mediaType;
final isImage =
@ -487,13 +486,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (pickedFile.name.contains('.gif')) {
mediaType = MediaType.gif;
}
imageBytes = pickedFile.readAsBytes();
image = ScreenshotImage(imageBytesFuture: pickedFile.readAsBytes());
} else {
videoFilePath = File(pickedFile.path);
}
await pushMediaEditor(
imageBytes,
image,
videoFilePath,
sharedFromGallery: true,
mediaType: mediaType,

View file

@ -5,13 +5,13 @@ import 'package:drift/drift.dart' show Value;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart';

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
@ -15,7 +16,7 @@ class SaveToGalleryButton extends StatefulWidget {
required this.mediaService,
super.key,
});
final Future<bool> Function() storeImageAsOriginal;
final Future<Uint8List?> Function() storeImageAsOriginal;
final bool displayButtonLabel;
final MediaFileService mediaService;
final bool isLoading;

View file

@ -3,8 +3,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hand_signature/signature.dart';
// ignore: implementation_imports
import 'package:hand_signature/src/utils.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';

View file

@ -6,7 +6,6 @@ import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -15,6 +14,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
@ -33,14 +33,15 @@ List<Layer> undoLayers = [];
List<Layer> removedLayers = [];
class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView(
{required this.sharedFromGallery,
const ShareImageEditorView({
required this.sharedFromGallery,
required this.mediaFileService,
super.key,
this.imageBytesFuture,
this.sendToGroup,
this.mainCameraController});
final Future<Uint8List?>? imageBytesFuture;
this.mainCameraController,
});
final ScreenshotImage? imageBytesFuture;
final Group? sendToGroup;
final bool sharedFromGallery;
final MediaFileService mediaFileService;
@ -84,9 +85,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
loadImage(widget.imageBytesFuture!);
} else {
if (widget.mediaFileService.tempPath.existsSync()) {
loadImage(widget.mediaFileService.tempPath.readAsBytes());
loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath));
} else if (widget.mediaFileService.originalPath.existsSync()) {
loadImage(widget.mediaFileService.originalPath.readAsBytes());
loadImage(
ScreenshotImage(file: widget.mediaFileService.originalPath),
);
}
}
}
@ -383,11 +386,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
}
Future<Uint8List?> getEditedImageBytes() async {
Future<ScreenshotImage?> getEditedImageBytes() async {
if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes;
return image;
return ScreenshotImage(imageBytes: image);
}
}
@ -412,22 +415,31 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return image;
}
Future<bool> storeImageAsOriginal() async {
Future<Uint8List?> storeImageAsOriginal() async {
if (mediaService.overlayImagePath.existsSync()) {
mediaService.overlayImagePath.deleteSync();
}
if (mediaService.tempPath.existsSync()) {
mediaService.tempPath.deleteSync();
}
if (mediaService.originalPath.existsSync()) {
mediaService.originalPath.deleteSync();
}
var bytes = imageBytes;
if (media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
} else {
final imageBytes = await getEditedImageBytes();
if (imageBytes == null) return false;
final image = await getEditedImageBytes();
if (image == null) return null;
bytes = await image.getBytes();
if (bytes == null) {
Log.error('imageBytes are empty');
return null;
}
if (media.type == MediaType.image || media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes);
mediaService.originalPath.writeAsBytesSync(bytes!);
} else if (media.type == MediaType.video) {
mediaService.overlayImagePath.writeAsBytesSync(imageBytes);
mediaService.overlayImagePath.writeAsBytesSync(bytes!);
} else {
Log.error('MediaType not supported: ${media.type}');
}
@ -447,12 +459,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
.renameSync(MediaFileService(mediaFile).storedPath.path);
}
}
return true;
return bytes;
}
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
imageBytes = await imageBytesFuture;
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
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
@ -486,18 +497,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
sendingOrLoadingImage = true;
});
await storeImageAsOriginal();
if (!context.mounted) return;
// Insert media file into the messages database and start uploading process in the background
await insertMediaFileInMessagesTable(
unawaited(
insertMediaFileInMessagesTable(
mediaService,
[widget.sendToGroup!.groupId],
storeImageAsOriginal(),
),
);
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context, true);
}
}

View file

@ -2,11 +2,13 @@
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';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
@ -26,7 +28,7 @@ class ShareImageView extends StatefulWidget {
});
final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
final Future<bool>? mediaStoreFuture;
final Future<Uint8List?>? mediaStoreFuture;
final MediaFileService mediaFileService;
@override
@ -41,6 +43,7 @@ class _ShareImageView extends State<ShareImageView> {
bool sendingImage = false;
bool mediaStoreFutureReady = false;
Uint8List? _imageBytes;
bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Group>> allGroupSub;
@ -63,10 +66,9 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> initAsync() async {
if (widget.mediaStoreFuture != null) {
await widget.mediaStoreFuture;
_imageBytes = await widget.mediaStoreFuture;
}
mediaStoreFutureReady = true;
// unawaited(startBackgroundMediaUpload(widget.mediaFileService));
if (!mounted) return;
setState(() {});
}
@ -235,12 +237,31 @@ class _ShareImageView extends State<ShareImageView> {
),
),
floatingActionButton: SizedBox(
height: 120,
height: 148,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
_imageBytes != null &&
gUser.showShowImagePreviewWhenSending)
SizedBox(
height: 100,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border:
Border.all(color: context.color.primary, width: 3),
color: context.color.primary,
borderRadius: BorderRadius.circular(10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(7),
child: Image.memory(_imageBytes!),
),
),
),
FilledButton.icon(
icon: !mediaStoreFutureReady || sendingImage
? SizedBox(
@ -265,6 +286,7 @@ class _ShareImageView extends State<ShareImageView> {
await insertMediaFileInMessagesTable(
widget.mediaFileService,
widget.selectedGroupIds.toList(),
null,
);
if (context.mounted) {
@ -288,7 +310,7 @@ class _ShareImageView extends State<ShareImageView> {
),
),
label: Text(
context.lang.shareImagedEditorSendImage,
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
style: const TextStyle(fontSize: 17),
),
),

View file

@ -151,6 +151,7 @@ class _MessageInputState extends State<MessageInput> {
await insertMediaFileInMessagesTable(
mediaFileService,
[widget.group.groupId],
null,
);
}

View file

@ -174,7 +174,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
);
};
}
if (mediaFile.uploadState == UploadState.preprocessing) {
if (mediaFile.uploadState == UploadState.preprocessing ||
mediaFile.uploadState == UploadState.initialized) {
text = context.lang.inProcess;
}
}

View file

@ -92,6 +92,16 @@ class _AppearanceViewState extends State<AppearanceView> {
});
}
Future<void> toggleShowImagePreviewWhenSending() async {
await updateUserdata((u) {
u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending;
return u;
});
setState(() {
// gUser
});
}
@override
Widget build(BuildContext context) {
final selectedTheme = context.watch<SettingsChangeProvider>().themeMode;
@ -127,6 +137,14 @@ class _AppearanceViewState extends State<AppearanceView> {
onChanged: (a) => toggleStartWithCameraOpen(),
),
),
ListTile(
title: Text(context.lang.showImagePreviewWhenSending),
onTap: toggleShowImagePreviewWhenSending,
trailing: Switch(
value: gUser.showShowImagePreviewWhenSending,
onChanged: (a) => toggleShowImagePreviewWhenSending(),
),
),
],
),
);

View file

@ -882,10 +882,9 @@ packages:
hand_signature:
dependency: "direct main"
description:
name: hand_signature
sha256: "05b40d3b2d1885a5dda126f26db386660aa46e497b63c96feb91d3198a667eea"
url: "https://pub.dev"
source: hosted
path: "dependencies/hand_signature"
relative: true
source: path
version: "3.1.0+2"
hashlib:
dependency: "direct main"
@ -1532,14 +1531,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
screenshot:
dependency: "direct main"
description:
name: screenshot
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
scrollable_positioned_list:
dependency: "direct main"
description:

View file

@ -78,13 +78,11 @@ dependencies:
gal: ^2.3.1
get: ^4.7.2
google_mlkit_barcode_scanning: ^0.14.1
hand_signature: ^3.0.3
image: ^4.3.0
no_screenshot: ^0.3.1
permission_handler: ^12.0.0+1
provider: ^6.1.2
restart_app: ^1.3.2
screenshot: ^3.0.0
sentry_flutter: ^9.8.0
app_links: ^7.0.0
in_app_purchase: ^3.2.3
@ -101,6 +99,7 @@ dependencies:
mutex: ^3.1.0
introduction_screen: ^4.0.0
qr_flutter: ^4.1.0
hand_signature: ^3.0.3
dependency_overrides:
dots_indicator:
@ -123,6 +122,8 @@ dependency_overrides:
path: ./dependencies/adaptive_number
ed25519_edwards:
path: ./dependencies/ed25519_edwards
hand_signature:
path: ./dependencies/hand_signature
hashlib_codecs:
path: ./dependencies/hashlib_codecs
optional: