From 35152cce23957630ba60b005b316ed0ae906c6a9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Oct 2025 23:26:24 +0100 Subject: [PATCH] integrating ffmpeg --- CHANGELOG.md | 22 +-- ios/Podfile.lock | 15 +- lib/src/database/tables/mediafiles.table.dart | 1 + lib/src/database/twonly.db.g.dart | 61 +++++++ .../api/mediafiles/upload.service.dart | 3 + .../contact.server_messages.dart | 17 ++ .../mediafiles/compression.service.dart | 76 ++++---- .../mediafiles/mediafile.service.dart | 26 ++- .../mediafiles/thumbnail.service.dart | 32 ++-- .../zoom_selector.dart | 7 +- .../camera_preview_controller_view.dart | 164 +++++++----------- lib/src/views/camera/camera_send_to_view.dart | 6 +- .../views/camera/image_editor/data/layer.dart | 4 +- .../image_editor/layers/filter_layer.dart | 1 + .../best_friends_selector.dart | 5 +- .../views/camera/share_image_editor_view.dart | 75 +++++--- lib/src/views/camera/share_image_view.dart | 5 +- .../chat_text_entry.dart | 10 +- lib/src/views/components/better_text.dart | 7 +- lib/src/views/home.view.dart | 13 +- .../updates/62_database_migration.view.dart | 2 +- pubspec.lock | 36 ++-- pubspec.yaml | 2 +- 23 files changed, 351 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e04885f..0fdcb3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.0.62 -- Support for Groups -- Editing of text messages -- Deletion of messages -- Various UI improvements like a new context-menu -- Client-to-client (C2C) protocol converted to ProtoBuf -- Use of UUIDs in the database -- Completely new database schema -- Improved reliability of C2C messages -- Improved video handling -- Various bug fixes +- Support for groups +- Edit & Delete messages +- Switched to FFmpeg for improved video compression + - Video max. length increased to 60 seconds + - Removing audio after recording is possible + - Edited image is now embedded into the video +- New context menu and other UI enhancements +- Client-to-client protocol migrated to Protocol Buffers (Protobuf) +- Database identifiers converted to UUIDs +- Completely redesigned database schema +- Improved reliability of client-to-client messaging +- Multiple bug fixes ## 0.0.61 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3d0a176..00f096a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,11 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - ffmpeg_kit_flutter_new_min_gpl (7.1.1): + - ffmpeg_kit_flutter_new_min_gpl/min-gpl (= 7.1.1) + - Flutter + - ffmpeg_kit_flutter_new_min_gpl/min-gpl (7.1.1): + - Flutter - Firebase (12.4.0): - Firebase/Core (= 12.4.0) - Firebase/Core (12.4.0): @@ -233,8 +238,6 @@ PODS: - SwiftProtobuf (1.32.0) - url_launcher_ios (0.0.1): - Flutter - - video_compress (0.3.0): - - Flutter - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS @@ -248,6 +251,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - ffmpeg_kit_flutter_new_min_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) @@ -275,7 +279,6 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - SwiftProtobuf - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) @@ -312,6 +315,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cryptography_flutter_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + ffmpeg_kit_flutter_new_min_gpl: + :path: ".symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -354,8 +359,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - video_compress: - :path: ".symlinks/plugins/video_compress/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" video_thumbnail: @@ -367,6 +370,7 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + ffmpeg_kit_flutter_new_min_gpl: 79212bc20869b4e12ec06705724c26b016e9d58e Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 @@ -407,7 +411,6 @@ SPEC CHECKSUMS: sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftProtobuf: 81e341191afbddd64aa031bd12862dccfab2f639 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b - video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 15ba964..585b49d 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -52,6 +52,7 @@ class MediaFiles extends Table { text().map(IntListTypeConverter()).nullable()(); IntColumn get displayLimitInMilliseconds => integer().nullable()(); + BoolColumn get removeAudio => boolean().nullable()(); BlobColumn get downloadToken => blob().nullable()(); BlobColumn get encryptionKey => blob().nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 9c63e40..cfe85e9 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1561,6 +1561,15 @@ class $MediaFilesTable extends MediaFiles late final GeneratedColumn displayLimitInMilliseconds = GeneratedColumn('display_limit_in_milliseconds', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _removeAudioMeta = + const VerificationMeta('removeAudio'); + @override + late final GeneratedColumn removeAudio = GeneratedColumn( + 'remove_audio', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("remove_audio" IN (0, 1))')); static const VerificationMeta _downloadTokenMeta = const VerificationMeta('downloadToken'); @override @@ -1604,6 +1613,7 @@ class $MediaFilesTable extends MediaFiles stored, reuploadRequestedBy, displayLimitInMilliseconds, + removeAudio, downloadToken, encryptionKey, encryptionMac, @@ -1649,6 +1659,12 @@ class $MediaFilesTable extends MediaFiles data['display_limit_in_milliseconds']!, _displayLimitInMillisecondsMeta)); } + if (data.containsKey('remove_audio')) { + context.handle( + _removeAudioMeta, + removeAudio.isAcceptableOrUnknown( + data['remove_audio']!, _removeAudioMeta)); + } if (data.containsKey('download_token')) { context.handle( _downloadTokenMeta, @@ -1709,6 +1725,8 @@ class $MediaFilesTable extends MediaFiles displayLimitInMilliseconds: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}display_limit_in_milliseconds']), + removeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}remove_audio']), downloadToken: attachedDatabase.typeMapping .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), encryptionKey: attachedDatabase.typeMapping @@ -1756,6 +1774,7 @@ class MediaFile extends DataClass implements Insertable { final bool stored; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; + final bool? removeAudio; final Uint8List? downloadToken; final Uint8List? encryptionKey; final Uint8List? encryptionMac; @@ -1771,6 +1790,7 @@ class MediaFile extends DataClass implements Insertable { required this.stored, this.reuploadRequestedBy, this.displayLimitInMilliseconds, + this.removeAudio, this.downloadToken, this.encryptionKey, this.encryptionMac, @@ -1804,6 +1824,9 @@ class MediaFile extends DataClass implements Insertable { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds); } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable(removeAudio); + } if (!nullToAbsent || downloadToken != null) { map['download_token'] = Variable(downloadToken); } @@ -1840,6 +1863,9 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds == null && nullToAbsent ? const Value.absent() : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), downloadToken: downloadToken == null && nullToAbsent ? const Value.absent() : Value(downloadToken), @@ -1875,6 +1901,7 @@ class MediaFile extends DataClass implements Insertable { serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: serializer.fromJson(json['displayLimitInMilliseconds']), + removeAudio: serializer.fromJson(json['removeAudio']), downloadToken: serializer.fromJson(json['downloadToken']), encryptionKey: serializer.fromJson(json['encryptionKey']), encryptionMac: serializer.fromJson(json['encryptionMac']), @@ -1899,6 +1926,7 @@ class MediaFile extends DataClass implements Insertable { 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), + 'removeAudio': serializer.toJson(removeAudio), 'downloadToken': serializer.toJson(downloadToken), 'encryptionKey': serializer.toJson(encryptionKey), 'encryptionMac': serializer.toJson(encryptionMac), @@ -1917,6 +1945,7 @@ class MediaFile extends DataClass implements Insertable { bool? stored, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -1938,6 +1967,7 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds: displayLimitInMilliseconds.present ? displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, downloadToken: downloadToken.present ? downloadToken.value : this.downloadToken, encryptionKey: @@ -1971,6 +2001,8 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds: data.displayLimitInMilliseconds.present ? data.displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, + removeAudio: + data.removeAudio.present ? data.removeAudio.value : this.removeAudio, downloadToken: data.downloadToken.present ? data.downloadToken.value : this.downloadToken, @@ -1999,6 +2031,7 @@ class MediaFile extends DataClass implements Insertable { ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') ..write('encryptionMac: $encryptionMac, ') @@ -2019,6 +2052,7 @@ class MediaFile extends DataClass implements Insertable { stored, reuploadRequestedBy, displayLimitInMilliseconds, + removeAudio, $driftBlobEquality.hash(downloadToken), $driftBlobEquality.hash(encryptionKey), $driftBlobEquality.hash(encryptionMac), @@ -2037,6 +2071,7 @@ class MediaFile extends DataClass implements Insertable { other.stored == this.stored && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && @@ -2055,6 +2090,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value stored; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; + final Value removeAudio; final Value downloadToken; final Value encryptionKey; final Value encryptionMac; @@ -2071,6 +2107,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), this.encryptionMac = const Value.absent(), @@ -2088,6 +2125,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), this.encryptionMac = const Value.absent(), @@ -2106,6 +2144,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? stored, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, + Expression? removeAudio, Expression? downloadToken, Expression? encryptionKey, Expression? encryptionMac, @@ -2126,6 +2165,7 @@ class MediaFilesCompanion extends UpdateCompanion { 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, if (downloadToken != null) 'download_token': downloadToken, if (encryptionKey != null) 'encryption_key': encryptionKey, if (encryptionMac != null) 'encryption_mac': encryptionMac, @@ -2145,6 +2185,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? stored, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, + Value? removeAudio, Value? downloadToken, Value? encryptionKey, Value? encryptionMac, @@ -2163,6 +2204,7 @@ class MediaFilesCompanion extends UpdateCompanion { reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, downloadToken: downloadToken ?? this.downloadToken, encryptionKey: encryptionKey ?? this.encryptionKey, encryptionMac: encryptionMac ?? this.encryptionMac, @@ -2209,6 +2251,9 @@ class MediaFilesCompanion extends UpdateCompanion { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds.value); } + if (removeAudio.present) { + map['remove_audio'] = Variable(removeAudio.value); + } if (downloadToken.present) { map['download_token'] = Variable(downloadToken.value); } @@ -2242,6 +2287,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') ..write('encryptionMac: $encryptionMac, ') @@ -7795,6 +7841,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, + Value removeAudio, Value downloadToken, Value encryptionKey, Value encryptionMac, @@ -7812,6 +7859,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, + Value removeAudio, Value downloadToken, Value encryptionKey, Value encryptionMac, @@ -7887,6 +7935,9 @@ class $$MediaFilesTableFilterComposer column: $table.displayLimitInMilliseconds, builder: (column) => ColumnFilters(column)); + ColumnFilters get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnFilters(column)); @@ -7966,6 +8017,9 @@ class $$MediaFilesTableOrderingComposer column: $table.displayLimitInMilliseconds, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnOrderings(column)); @@ -8025,6 +8079,9 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get displayLimitInMilliseconds => $composableBuilder( column: $table.displayLimitInMilliseconds, builder: (column) => column); + GeneratedColumn get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => column); @@ -8094,6 +8151,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -8111,6 +8169,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, downloadToken: downloadToken, encryptionKey: encryptionKey, encryptionMac: encryptionMac, @@ -8128,6 +8187,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -8145,6 +8205,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, downloadToken: downloadToken, encryptionKey: encryptionKey, encryptionMac: encryptionMac, diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 55a7ba0..664ea30 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -74,6 +74,7 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { if (mediaService.mediaFile.uploadState == UploadState.initialized || mediaService.mediaFile.uploadState == UploadState.preprocessing) { await mediaService.setUploadState(UploadState.preprocessing); + if (!mediaService.tempPath.existsSync()) { await mediaService.compressMedia(); } @@ -87,6 +88,8 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { } if (mediaService.uploadRequestPath.existsSync()) { await mediaService.setUploadState(UploadState.uploading); + // at this point the original file is not used any more, so it can be deleted + mediaService.originalPath.deleteSync(); } } diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index c49ac82..8e5aaa3 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -20,6 +20,23 @@ Future handleContactRequest( switch (contactRequest.type) { case EncryptedContent_ContactRequest_Type.REQUEST: Log.info('Got a contact request from $fromUserId'); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.accepted) { + // contact was already accepted, so just accept the request in the background. + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.ACCEPT, + ), + ), + ); + return; + } + } // Request the username by the server so an attacker can not // forge the displayed username in the contact request final username = await apiService.getUsername(fromUserId); diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index 8be5e6f..b09f33e 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -1,8 +1,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:drift/drift.dart' show Value; +import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new/return_code.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:video_compress/video_compress.dart'; Future compressImage( File sourceFile, @@ -10,6 +16,8 @@ Future compressImage( ) async { final stopwatch = Stopwatch()..start(); + // // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp + try { var compressedBytes = await FlutterImageCompress.compressWithFile( sourceFile.path, @@ -53,42 +61,40 @@ Future compressImage( ); } -Future compressVideo( - File sourceFile, - File destinationFile, -) async { - final stopwatch = Stopwatch()..start(); - - MediaInfo? mediaInfo; - try { - mediaInfo = await VideoCompress.compressVideo( - sourceFile.path, - quality: VideoQuality.Res1280x720Quality, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 - ); - - Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); - - if (mediaInfo.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - sourceFile.path, - quality: VideoQuality.Res960x540Quality, - includeAudio: true, - ); - } - } catch (e) { - Log.error('during video compression: $e'); +Future compressAndOverlayVideo(MediaFileService media) async { + if (media.tempPath.existsSync()) { + media.tempPath.deleteSync(); } - stopwatch.stop(); - Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); - if (mediaInfo == null) { - Log.error('Could not compress video using original video.'); - // as a fall back use the non compressed version - sourceFile.copySync(destinationFile.path); + final stopwatch = Stopwatch()..start(); + var command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.tempPath.path}"'; + + if (media.removeAudio) { + command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.tempPath.path}"'; + } + + final session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video', + ); } else { - await mediaInfo.file!.copy(destinationFile.path); + Log.info(command); + Log.error('Compression failed for the video with exit code $returnCode.'); + Log.error(await session.getAllLogsAsString()); + // This should not happen, but in case "notify" the user that the video was not send... This is absolutely bad, but + // better this way then sending an uncompressed media file which potentially is 100MB big :/ + // Hopefully the user will report the strange behavior <3 + await twonlyDB.messagesDao.updateMessagesByMediaId( + media.mediaFile.mediaId, + const MessagesCompanion(isDeletedFromSender: Value(true)), + ); + media.fullMediaRemoval(); + await media.setUploadState(UploadState.uploaded); } } diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index e2852d6..0b11ae4 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -107,6 +107,22 @@ class MediaFileService { await updateFromDB(); } + bool get removeAudio => mediaFile.removeAudio ?? false; + + Future toggleRemoveAudio() async { + // var removeAudio = false; + // if (mediaFile.removeAudio != null) { + // removeAudio = !mediaFile.removeAudio!; + // } + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + removeAudio: Value(!removeAudio), + ), + ); + await updateFromDB(); + } + Future setUploadState(UploadState uploadState) async { await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, @@ -160,11 +176,12 @@ class MediaFileService { Log.error('Could not compress as original media does not exists.'); return; } + switch (mediaFile.type) { case MediaType.image: await compressImage(originalPath, tempPath); case MediaType.video: - await compressVideo(originalPath, tempPath); + await compressAndOverlayVideo(this); case MediaType.gif: originalPath.renameSync(tempPath.path); Log.error('Compression for .gif is not implemented yet.'); @@ -266,7 +283,7 @@ class MediaFileService { File get thumbnailPath => _buildFilePath( 'stored', namePrefix: '.thumbnail', - extensionParam: 'png', + extensionParam: 'webp', ); File get encryptedPath => _buildFilePath( 'tmp', @@ -280,4 +297,9 @@ class MediaFileService { 'tmp', namePrefix: '.original', ); + File get overlayImagePath => _buildFilePath( + 'tmp', + namePrefix: '.overlay', + extensionParam: 'png', + ); } diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index e73c8e4..cc3437e 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -1,25 +1,29 @@ import 'dart:io'; +import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new/return_code.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:video_thumbnail/video_thumbnail.dart'; Future createThumbnailsForVideo( File sourceFile, File destinationFile, ) async { - final fileExtension = sourceFile.path.split('.').last.toLowerCase(); - if (fileExtension != 'mp4') { - Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); - return; - } + final stopwatch = Stopwatch()..start(); - try { - await VideoThumbnail.thumbnailFile( - video: sourceFile.path, - thumbnailPath: destinationFile.path, - maxWidth: 450, - quality: 75, + final command = + '-i ${sourceFile.path} -ss 00:00:00 -vframes 1 -vf "scale=iw:ih:flags=lanczos" -c:v libwebp -q:v 100 -compression_level 6 ${destinationFile.path}'; + + final session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', ); - } catch (e) { - Log.error('Could not create the video thumbnail: $e'); + } else { + Log.info(command); + Log.error('Compression failed for the video with exit code $returnCode.'); + Log.error(await session.getAllLogsAsString()); + // Report this error to the user? } } diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index 499a335..3d4f944 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -23,8 +23,7 @@ class CameraZoomButtons extends StatefulWidget { final double scaleFactor; final Function updateScaleFactor; final SelectedCameraDetails selectedCameraDetails; - final Future Function(int sCameraId, bool init, bool enableAudio) - selectCamera; + final Future Function(int sCameraId, bool init) selectCamera; @override State createState() => _CameraZoomButtonsState(); @@ -106,7 +105,7 @@ class _CameraZoomButtonsState extends State { ), onPressed: () async { if (showWideAngleZoomIOS) { - await widget.selectCamera(2, true, false); + await widget.selectCamera(2, true); } else { final level = await widget.controller.getMinZoomLevel(); widget.updateScaleFactor(level); @@ -130,7 +129,7 @@ class _CameraZoomButtonsState extends State { onPressed: () async { if (showWideAngleZoomIOS && widget.selectedCameraDetails.cameraId == 2) { - await widget.selectCamera(0, true, false); + await widget.selectCamera(0, true); } else { widget.updateScaleFactor(1.0); } diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index edea3bc..4ab20d1 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -23,13 +23,12 @@ import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; -int maxVideoRecordingTime = 15; +int maxVideoRecordingTime = 60; Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( SelectedCameraDetails details, int sCameraId, bool init, - bool enableAudio, ) async { var cameraId = sCameraId; if (cameraId >= gCameras.length) return null; @@ -49,7 +48,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( final cameraController = CameraController( gCameras[cameraId], ResolutionPreset.high, - enableAudio: enableAudio, + enableAudio: await Permission.microphone.isGranted, ); await cameraController.initialize().then((_) async { @@ -93,11 +92,8 @@ class CameraPreviewControllerView extends StatelessWidget { this.sendToGroup, }); final Group? sendToGroup; - final Future Function( - int sCameraId, - bool init, - bool enableAudio, - ) selectCamera; + final Future Function(int sCameraId, bool init) + selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; @@ -119,8 +115,7 @@ class CameraPreviewControllerView extends StatelessWidget { } else { return PermissionHandlerView( onSuccess: () { - // setState(() {}); - selectCamera(0, true, false); + selectCamera(0, true); }, ); } @@ -145,7 +140,6 @@ class CameraPreviewView extends StatefulWidget { final Future Function( int sCameraId, bool init, - bool enableAudio, ) selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; @@ -156,19 +150,17 @@ class CameraPreviewView extends StatefulWidget { } class _CameraPreviewViewState extends State { - bool sharePreviewIsShown = false; - bool galleryLoadedImageIsShown = false; - bool showSelfieFlash = false; - double basePanY = 0; - double baseScaleFactor = 0; - bool cameraLoaded = false; - bool isVideoRecording = false; - bool hasAudioPermission = true; - bool videoWithAudio = true; - DateTime? videoRecordingStarted; - Timer? videoRecordingTimer; + bool _sharePreviewIsShown = false; + bool _galleryLoadedImageIsShown = false; + bool _showSelfieFlash = false; + double _basePanY = 0; + double _baseScaleFactor = 0; + bool _isVideoRecording = false; + bool _hasAudioPermission = true; + DateTime? _videoRecordingStarted; + Timer? _videoRecordingTimer; - DateTime currentTime = DateTime.now(); + DateTime _currentTime = DateTime.now(); final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); @@ -179,9 +171,9 @@ class _CameraPreviewViewState extends State { } Future initAsync() async { - hasAudioPermission = await Permission.microphone.isGranted; + _hasAudioPermission = await Permission.microphone.isGranted; - if (!hasAudioPermission && !gUser.requestedAudioPermission) { + if (!_hasAudioPermission && !gUser.requestedAudioPermission) { await updateUserdata((u) { u.requestedAudioPermission = true; return u; @@ -194,7 +186,7 @@ class _CameraPreviewViewState extends State { @override void dispose() { - videoRecordingTimer?.cancel(); + _videoRecordingTimer?.cancel(); super.dispose(); } @@ -205,7 +197,7 @@ class _CameraPreviewViewState extends State { if (statuses[Permission.microphone]!.isPermanentlyDenied) { await openAppSettings(); } else { - hasAudioPermission = await Permission.microphone.isGranted; + _hasAudioPermission = await Permission.microphone.isGranted; setState(() {}); } } @@ -248,16 +240,16 @@ class _CameraPreviewViewState extends State { } Future takePicture() async { - if (sharePreviewIsShown || isVideoRecording) return; + if (_sharePreviewIsShown || _isVideoRecording) return; late Future imageBytes; setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); if (widget.selectedCameraDetails.isFlashOn) { if (isFront) { setState(() { - showSelfieFlash = true; + _showSelfieFlash = true; }); } else { await widget.cameraController?.setFlashMode(FlashMode.torch); @@ -285,7 +277,7 @@ class _CameraPreviewViewState extends State { return; } setState(() { - sharePreviewIsShown = false; + _sharePreviewIsShown = false; }); } @@ -311,7 +303,7 @@ class _CameraPreviewViewState extends State { ..deleteSync(); // Start with compressing the video, to speed up the process in case the video is not changed. - unawaited(mediaFileService.compressMedia()); + // unawaited(mediaFileService.compressMedia()); } final shouldReturn = await Navigator.push( @@ -333,8 +325,8 @@ class _CameraPreviewViewState extends State { ) as bool?; if (mounted) { setState(() { - sharePreviewIsShown = false; - showSelfieFlash = false; + _sharePreviewIsShown = false; + _showSelfieFlash = false; }); } if (!mounted) return true; @@ -350,7 +342,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( widget.selectedCameraDetails.cameraId, false, - false, ); return false; } @@ -368,9 +359,9 @@ class _CameraPreviewViewState extends State { return; } - widget.selectedCameraDetails.scaleFactor = (baseScaleFactor + + widget.selectedCameraDetails.scaleFactor = (_baseScaleFactor + // ignore: avoid_dynamic_calls - (basePanY - (details.localPosition.dy as double)) / 30) + (_basePanY - (details.localPosition.dy as double)) / 30) .clamp(1, widget.selectedCameraDetails.maxAvailableZoom); await widget.cameraController! @@ -382,8 +373,8 @@ class _CameraPreviewViewState extends State { Future pickImageFromGallery() async { setState(() { - galleryLoadedImageIsShown = true; - sharePreviewIsShown = true; + _galleryLoadedImageIsShown = true; + _sharePreviewIsShown = true; }); final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); @@ -397,8 +388,8 @@ class _CameraPreviewViewState extends State { ); } setState(() { - galleryLoadedImageIsShown = false; - sharePreviewIsShown = false; + _galleryLoadedImageIsShown = false; + _sharePreviewIsShown = false; }); } @@ -407,41 +398,32 @@ class _CameraPreviewViewState extends State { widget.cameraController!.value.isRecordingVideo) { return; } - var cameraController = widget.cameraController; - if (hasAudioPermission && videoWithAudio) { - cameraController = await widget.selectCamera( - widget.selectedCameraDetails.cameraId, - false, - await Permission.microphone.isGranted && videoWithAudio, - ); - } - setState(() { - isVideoRecording = true; + _isVideoRecording = true; }); try { - await cameraController?.startVideoRecording(); - videoRecordingTimer = + await widget.cameraController?.startVideoRecording(); + _videoRecordingTimer = Timer.periodic(const Duration(milliseconds: 15), (timer) { setState(() { - currentTime = DateTime.now(); + _currentTime = DateTime.now(); }); - if (videoRecordingStarted != null && - currentTime.difference(videoRecordingStarted!).inSeconds >= + if (_videoRecordingStarted != null && + _currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) { timer.cancel(); - videoRecordingTimer = null; + _videoRecordingTimer = null; stopVideoRecording(); } }); setState(() { - videoRecordingStarted = DateTime.now(); - isVideoRecording = true; + _videoRecordingStarted = DateTime.now(); + _isVideoRecording = true; }); } on CameraException catch (e) { setState(() { - isVideoRecording = false; + _isVideoRecording = false; }); _showCameraException(e); return; @@ -449,14 +431,14 @@ class _CameraPreviewViewState extends State { } Future stopVideoRecording() async { - if (videoRecordingTimer != null) { - videoRecordingTimer?.cancel(); - videoRecordingTimer = null; + if (_videoRecordingTimer != null) { + _videoRecordingTimer?.cancel(); + _videoRecordingTimer = null; } setState(() { - videoRecordingStarted = null; - isVideoRecording = false; + _videoRecordingStarted = null; + _isVideoRecording = false; }); if (widget.cameraController == null || @@ -465,7 +447,7 @@ class _CameraPreviewViewState extends State { } setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); try { @@ -509,15 +491,15 @@ class _CameraPreviewViewState extends State { return; } setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); }, onLongPressMoveUpdate: onPanUpdate, onLongPressStart: (details) { setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); // Get the position of the pointer final renderBox = @@ -540,7 +522,7 @@ class _CameraPreviewViewState extends State { onPanUpdate: onPanUpdate, child: Stack( children: [ - if (galleryLoadedImageIsShown) + if (_galleryLoadedImageIsShown) Center( child: SizedBox( width: 20, @@ -551,11 +533,11 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!sharePreviewIsShown && + if (!_sharePreviewIsShown && widget.sendToGroup != null && - !isVideoRecording) + !_isVideoRecording) SendToWidget(sendTo: widget.sendToGroup!.groupName), - if (!sharePreviewIsShown && !isVideoRecording) + if (!_sharePreviewIsShown && !_isVideoRecording) Positioned( right: 5, top: 0, @@ -573,7 +555,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( (widget.selectedCameraDetails.cameraId + 1) % 2, false, - false, ); }, ), @@ -598,7 +579,7 @@ class _CameraPreviewViewState extends State { setState(() {}); }, ), - if (!hasAudioPermission) + if (!_hasAudioPermission) ActionButton( Icons.mic_off_rounded, color: Colors.white.withAlpha(160), @@ -606,27 +587,12 @@ class _CameraPreviewViewState extends State { 'Allow microphone access for video recording.', onPressed: requestMicrophonePermission, ), - if (hasAudioPermission) - ActionButton( - videoWithAudio - ? Icons.volume_up_rounded - : Icons.volume_off_rounded, - tooltipText: 'Record video with audio.', - color: videoWithAudio - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - setState(() { - videoWithAudio = !videoWithAudio; - }); - }, - ), ], ), ), ), ), - if (!sharePreviewIsShown) + if (!_sharePreviewIsShown) Positioned( bottom: 30, left: 0, @@ -638,7 +604,7 @@ class _CameraPreviewViewState extends State { if (widget.cameraController!.value.isInitialized && widget.selectedCameraDetails.isZoomAble && !isFront && - !isVideoRecording) + !_isVideoRecording) SizedBox( width: 120, child: CameraZoomButtons( @@ -655,7 +621,7 @@ class _CameraPreviewViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!isVideoRecording) + if (!_isVideoRecording) GestureDetector( onTap: pickImageFromGallery, child: Align( @@ -687,7 +653,7 @@ class _CameraPreviewViewState extends State { shape: BoxShape.circle, border: Border.all( width: 7, - color: isVideoRecording + color: _isVideoRecording ? Colors.red : Colors.white, ), @@ -695,7 +661,7 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!isVideoRecording) const SizedBox(width: 80), + if (!_isVideoRecording) const SizedBox(width: 80), ], ), ], @@ -703,10 +669,10 @@ class _CameraPreviewViewState extends State { ), ), VideoRecordingTimer( - videoRecordingStarted: videoRecordingStarted, + videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!sharePreviewIsShown && widget.sendToGroup != null) + if (!_sharePreviewIsShown && widget.sendToGroup != null) Positioned( left: 5, top: 10, @@ -718,7 +684,7 @@ class _CameraPreviewViewState extends State { }, ), ), - if (showSelfieFlash) + if (_showSelfieFlash) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(22), diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index 8586b4d..cb36407 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -22,7 +22,7 @@ class CameraSendToViewState extends State { @override void initState() { super.initState(); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); } @override @@ -36,13 +36,11 @@ class CameraSendToViewState extends State { Future selectCamera( int sCameraId, bool init, - bool enableAudio, ) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -61,7 +59,7 @@ class CameraSendToViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } @override diff --git a/lib/src/views/camera/image_editor/data/layer.dart b/lib/src/views/camera/image_editor/data/layer.dart index 707e348..7f6bdf8 100755 --- a/lib/src/views/camera/image_editor/data/layer.dart +++ b/lib/src/views/camera/image_editor/data/layer.dart @@ -34,7 +34,9 @@ class BackgroundLayerData extends Layer { ImageItem image; } -class FilterLayerData extends Layer {} +class FilterLayerData extends Layer { + int page = 1; +} /// Attributes used by [EmojiLayer] class EmojiLayerData extends Layer { diff --git a/lib/src/views/camera/image_editor/layers/filter_layer.dart b/lib/src/views/camera/image_editor/layers/filter_layer.dart index c0179d0..13e76a2 100644 --- a/lib/src/views/camera/image_editor/layers/filter_layer.dart +++ b/lib/src/views/camera/image_editor/layers/filter_layer.dart @@ -116,6 +116,7 @@ class _FilterLayerState extends State { } }, onPageChanged: (index) { + widget.layerData.page = index; if (index == 0) { // If the user swipes to the first duplicated page, jump to the last page pageController.jumpToPage(pages.length); diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart index 491abf9..7271518 100644 --- a/lib/src/views/camera/share_image_components/best_friends_selector.dart +++ b/lib/src/views/camera/share_image_components/best_friends_selector.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; @@ -148,9 +149,7 @@ class UserCheckbox extends StatelessWidget { Row( children: [ Text( - group.groupName.length > 12 - ? '${group.groupName.substring(0, 9)}...' - : group.groupName, + substringBy(group.groupName, 12), overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d84708d..32ff434 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -211,6 +211,25 @@ class _ShareImageEditorView extends State { }, ), ), + if (media.type == MediaType.video) + ActionButton( + (mediaService.removeAudio) + ? Icons.volume_off_rounded + : Icons.volume_up_rounded, + tooltipText: 'Enable Audio in Video', + color: (mediaService.removeAudio) + ? Colors.white.withAlpha(160) + : Colors.white, + onPressed: () async { + await mediaService.toggleRemoveAudio(); + if (mediaService.removeAudio) { + await videoController?.setVolume(0); + } else { + await videoController?.setVolume(100); + } + if (mounted) setState(() {}); + }, + ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.shieldHeart, @@ -281,8 +300,7 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - final mediaStoreFuture = - (media.type == MediaType.image) ? storeImageAsOriginal() : null; + final mediaStoreFuture = storeImageAsOriginal(); await videoController?.pause(); if (isDisposed || !mounted) return; @@ -312,33 +330,39 @@ class _ShareImageEditorView extends State { } } - if (layers.length > 1 || media.type == MediaType.video) { - for (final x in layers) { - x.showCustomButtons = false; - } - setState(() {}); - final image = await screenshotController.capture( - pixelRatio: pixelRatio, - ); - if (image == null) { - Log.error('screenshotController did not return image bytes'); - return null; - } - - for (final x in layers) { - x.showCustomButtons = true; - } - setState(() {}); - return image; + for (final x in layers) { + x.showCustomButtons = false; + } + setState(() {}); + final image = await screenshotController.capture( + pixelRatio: pixelRatio, + ); + if (image == null) { + Log.error('screenshotController did not return image bytes'); + return null; } - return null; + for (final x in layers) { + x.showCustomButtons = true; + } + setState(() {}); + return image; } Future storeImageAsOriginal() async { + if (mediaService.overlayImagePath.existsSync()) { + mediaService.overlayImagePath.deleteSync(); + } + if (mediaService.tempPath.existsSync()) { + mediaService.tempPath.deleteSync(); + } final imageBytes = await getEditedImageBytes(); if (imageBytes == null) return false; - mediaService.originalPath.writeAsBytesSync(imageBytes); + if (media.type == MediaType.image) { + mediaService.originalPath.writeAsBytesSync(imageBytes); + } else { + mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + } // In case the image was already stored, then rename the stored image. if (mediaService.storedPath.existsSync()) { @@ -373,12 +397,7 @@ class _ShareImageEditorView extends State { sendingOrLoadingImage = true; }); - if (media.type == MediaType.image) { - await storeImageAsOriginal(); - } - if (media.type == MediaType.video) { - Log.error('TODO: COMBINE VIDEO AND IMAGE!!!'); - } + await storeImageAsOriginal(); if (!context.mounted) return; diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 1db8c71..7acf01e 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -5,6 +5,7 @@ import 'dart:collection'; 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/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; @@ -64,7 +65,7 @@ class _ShareImageView extends State { await widget.mediaStoreFuture; } mediaStoreFutureReady = true; - unawaited(startBackgroundMediaUpload(widget.mediaFileService)); + // unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); } @@ -323,7 +324,7 @@ class UserList extends StatelessWidget { return ListTile( title: Row( children: [ - Text(group.groupName), + Text(substringBy(group.groupName, 12)), FlameCounterWidget( groupId: group.groupId, prefix: true, diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 121fa94..a56dcde 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -25,6 +25,7 @@ class ChatTextEntry extends StatelessWidget { @override Widget build(BuildContext context) { var text = message.content ?? ''; + var textColor = Colors.white; if (EmojiAnimation.supported(text)) { return Container( @@ -59,7 +60,10 @@ class ChatTextEntry extends StatelessWidget { if (message.isDeletedFromSender) { text = context.lang.messageWasDeleted; - color = Colors.grey; + color = isDarkMode(context) ? Colors.black : Colors.grey; + if (isDarkMode(context)) { + textColor = const Color.fromARGB(255, 99, 99, 99); + } } return Container( @@ -78,10 +82,10 @@ class ChatTextEntry extends StatelessWidget { children: [ if (expanded) Expanded( - child: BetterText(text: text), + child: BetterText(text: text, textColor: textColor), ) else ...[ - BetterText(text: text), + BetterText(text: text, textColor: textColor), SizedBox( width: spacerWidth, ), diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index 97399a2..7b4608f 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -5,8 +5,9 @@ import 'package:twonly/src/utils/log.dart'; import 'package:url_launcher/url_launcher.dart'; class BetterText extends StatelessWidget { - const BetterText({required this.text, super.key}); + const BetterText({required this.text, required this.textColor, super.key}); final String text; + final Color textColor; @override Widget build(BuildContext context) { @@ -65,8 +66,8 @@ class BetterText extends StatelessWidget { softWrap: true, textAlign: TextAlign.start, overflow: TextOverflow.visible, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: textColor, fontSize: 17, decoration: TextDecoration.none, fontWeight: FontWeight.normal, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index d7e3bd8..7571421 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -70,7 +70,7 @@ class HomeViewState extends State { } if (cameraController == null && !initCameraStarted && offsetRatio < 1) { initCameraStarted = true; - unawaited(selectCamera(selectedCameraDetails.cameraId, false, false)); + unawaited(selectCamera(selectedCameraDetails.cameraId, false)); } if (offsetRatio == 1) { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { @@ -97,7 +97,7 @@ class HomeViewState extends State { .listen((NotificationResponse? response) async { globalUpdateOfHomeViewPageIndex(0); }); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); unawaited(initAsync()); } @@ -109,16 +109,11 @@ class HomeViewState extends State { super.dispose(); } - Future selectCamera( - int sCameraId, - bool init, - bool enableAudio, - ) async { + Future selectCamera(int sCameraId, bool init) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -138,7 +133,7 @@ class HomeViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } Future initAsync() async { diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index 15821cb..b04a407 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -357,7 +357,7 @@ class _DatabaseMigrationViewState extends State { children: [ const SizedBox(height: 40), const Text( - 'twonly. Jetzt besser als je zuvor.', + 'twonly. Besser als je zuvor.', textAlign: TextAlign.center, style: TextStyle( fontSize: 35, diff --git a/pubspec.lock b/pubspec.lock index a1584b0..7e04a56 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "8.4.1" archive: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: camera - sha256: "87a27e0553e3432119c1c2f6e4b9a1bbf7d2c660552b910bfa59185a9facd632" + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab url: "https://pub.dev" source: hosted - version: "0.11.2+1" + version: "0.11.3" camera_android_camerax: dependency: "direct overridden" description: @@ -402,6 +402,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + ffmpeg_kit_flutter_new: + dependency: "direct main" + description: + name: ffmpeg_kit_flutter_new + sha256: d127635f27e93a7f21f0a14ce0a1a148e80919c402dac4a2118d73bfb17ce841 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + ffmpeg_kit_flutter_platform_interface: + dependency: transitive + description: + name: ffmpeg_kit_flutter_platform_interface + sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee + url: "https://pub.dev" + source: hosted + version: "0.2.1" file: dependency: transitive description: @@ -850,10 +866,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 url: "https://pub.dev" source: hosted - version: "0.8.13+5" + version: "0.8.13+7" image_picker_for_web: dependency: transitive description: @@ -1771,14 +1787,6 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.0" - video_compress: - dependency: "direct main" - description: - name: video_compress - sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" - url: "https://pub.dev" - source: hosted - version: "3.1.4" video_player: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f9a646d..bdde2b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: device_info_plus: ^12.1.0 drift: ^2.25.1 drift_flutter: ^0.2.4 + ffmpeg_kit_flutter_new: ^4.1.0 firebase_core: ^4.2.0 firebase_messaging: ^16.0.3 fixnum: ^1.1.1 @@ -67,7 +68,6 @@ dependencies: share_plus: ^12.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1 - video_compress: ^3.1.4 video_player: ^2.9.5 video_thumbnail: ^0.5.6 web_socket_channel: ^3.0.1