integrating ffmpeg

This commit is contained in:
otsmr 2025-10-28 23:26:24 +01:00
parent bd4c30ed4d
commit 35152cce23
23 changed files with 351 additions and 239 deletions

View file

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

View file

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

View file

@ -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()();

View file

@ -1561,6 +1561,15 @@ class $MediaFilesTable extends MediaFiles
late final GeneratedColumn<int> displayLimitInMilliseconds =
GeneratedColumn<int>('display_limit_in_milliseconds', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
static const VerificationMeta _removeAudioMeta =
const VerificationMeta('removeAudio');
@override
late final GeneratedColumn<bool> removeAudio = GeneratedColumn<bool>(
'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<MediaFile> {
final bool stored;
final List<int>? 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<MediaFile> {
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<MediaFile> {
map['display_limit_in_milliseconds'] =
Variable<int>(displayLimitInMilliseconds);
}
if (!nullToAbsent || removeAudio != null) {
map['remove_audio'] = Variable<bool>(removeAudio);
}
if (!nullToAbsent || downloadToken != null) {
map['download_token'] = Variable<Uint8List>(downloadToken);
}
@ -1840,6 +1863,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
displayLimitInMilliseconds:
serializer.fromJson<int?>(json['displayLimitInMilliseconds']),
removeAudio: serializer.fromJson<bool?>(json['removeAudio']),
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
encryptionKey: serializer.fromJson<Uint8List?>(json['encryptionKey']),
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
@ -1899,6 +1926,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
'displayLimitInMilliseconds':
serializer.toJson<int?>(displayLimitInMilliseconds),
'removeAudio': serializer.toJson<bool?>(removeAudio),
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
'encryptionKey': serializer.toJson<Uint8List?>(encryptionKey),
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
@ -1917,6 +1945,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
bool? stored,
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> encryptionMac = const Value.absent(),
@ -1938,6 +1967,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
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<MediaFile> {
..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<MediaFile> {
stored,
reuploadRequestedBy,
displayLimitInMilliseconds,
removeAudio,
$driftBlobEquality.hash(downloadToken),
$driftBlobEquality.hash(encryptionKey),
$driftBlobEquality.hash(encryptionMac),
@ -2037,6 +2071,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
final Value<bool> stored;
final Value<List<int>?> reuploadRequestedBy;
final Value<int?> displayLimitInMilliseconds;
final Value<bool?> removeAudio;
final Value<Uint8List?> downloadToken;
final Value<Uint8List?> encryptionKey;
final Value<Uint8List?> encryptionMac;
@ -2071,6 +2107,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
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<MediaFile> {
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<MediaFile> {
Expression<bool>? stored,
Expression<String>? reuploadRequestedBy,
Expression<int>? displayLimitInMilliseconds,
Expression<bool>? removeAudio,
Expression<Uint8List>? downloadToken,
Expression<Uint8List>? encryptionKey,
Expression<Uint8List>? encryptionMac,
@ -2126,6 +2165,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
'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<MediaFile> {
Value<bool>? stored,
Value<List<int>?>? reuploadRequestedBy,
Value<int?>? displayLimitInMilliseconds,
Value<bool?>? removeAudio,
Value<Uint8List?>? downloadToken,
Value<Uint8List?>? encryptionKey,
Value<Uint8List?>? encryptionMac,
@ -2163,6 +2204,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
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<MediaFile> {
map['display_limit_in_milliseconds'] =
Variable<int>(displayLimitInMilliseconds.value);
}
if (removeAudio.present) {
map['remove_audio'] = Variable<bool>(removeAudio.value);
}
if (downloadToken.present) {
map['download_token'] = Variable<Uint8List>(downloadToken.value);
}
@ -2242,6 +2287,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..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<bool> stored,
Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio,
Value<Uint8List?> downloadToken,
Value<Uint8List?> encryptionKey,
Value<Uint8List?> encryptionMac,
@ -7812,6 +7859,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<bool> stored,
Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio,
Value<Uint8List?> downloadToken,
Value<Uint8List?> encryptionKey,
Value<Uint8List?> encryptionMac,
@ -7887,6 +7935,9 @@ class $$MediaFilesTableFilterComposer
column: $table.displayLimitInMilliseconds,
builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get removeAudio => $composableBuilder(
column: $table.removeAudio, builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => ColumnFilters(column));
@ -7966,6 +8017,9 @@ class $$MediaFilesTableOrderingComposer
column: $table.displayLimitInMilliseconds,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get removeAudio => $composableBuilder(
column: $table.removeAudio, builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnOrderings(column));
@ -8025,6 +8079,9 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<int> get displayLimitInMilliseconds => $composableBuilder(
column: $table.displayLimitInMilliseconds, builder: (column) => column);
GeneratedColumn<bool> get removeAudio => $composableBuilder(
column: $table.removeAudio, builder: (column) => column);
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => column);
@ -8094,6 +8151,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<bool> stored = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> 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<bool> stored = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> 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,

View file

@ -74,6 +74,7 @@ Future<void> 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<void> 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();
}
}

View file

@ -20,6 +20,23 @@ Future<void> 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);

View file

@ -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<void> compressImage(
File sourceFile,
@ -10,6 +16,8 @@ Future<void> 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<void> compressImage(
);
}
Future<void> 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<void> 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);
}
}

View file

@ -107,6 +107,22 @@ class MediaFileService {
await updateFromDB();
}
bool get removeAudio => mediaFile.removeAudio ?? false;
Future<void> 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<void> 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',
);
}

View file

@ -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<void> 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?
}
}

View file

@ -23,8 +23,7 @@ class CameraZoomButtons extends StatefulWidget {
final double scaleFactor;
final Function updateScaleFactor;
final SelectedCameraDetails selectedCameraDetails;
final Future<void> Function(int sCameraId, bool init, bool enableAudio)
selectCamera;
final Future<void> Function(int sCameraId, bool init) selectCamera;
@override
State<CameraZoomButtons> createState() => _CameraZoomButtonsState();
@ -106,7 +105,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
),
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<CameraZoomButtons> {
onPressed: () async {
if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == 2) {
await widget.selectCamera(0, true, false);
await widget.selectCamera(0, true);
} else {
widget.updateScaleFactor(1.0);
}

View file

@ -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<CameraController?> Function(
int sCameraId,
bool init,
bool enableAudio,
) selectCamera;
final Future<CameraController?> 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<CameraController?> 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<CameraPreviewView> {
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -179,9 +171,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
Future<void> 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<CameraPreviewView> {
@override
void dispose() {
videoRecordingTimer?.cancel();
_videoRecordingTimer?.cancel();
super.dispose();
}
@ -205,7 +197,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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<CameraPreviewView> {
}
Future<void> takePicture() async {
if (sharePreviewIsShown || isVideoRecording) return;
if (_sharePreviewIsShown || _isVideoRecording) return;
late Future<Uint8List?> 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<CameraPreviewView> {
return;
}
setState(() {
sharePreviewIsShown = false;
_sharePreviewIsShown = false;
});
}
@ -311,7 +303,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
..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<CameraPreviewView> {
) 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<CameraPreviewView> {
await widget.selectCamera(
widget.selectedCameraDetails.cameraId,
false,
false,
);
return false;
}
@ -368,9 +359,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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<CameraPreviewView> {
Future<void> 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<CameraPreviewView> {
);
}
setState(() {
galleryLoadedImageIsShown = false;
sharePreviewIsShown = false;
_galleryLoadedImageIsShown = false;
_sharePreviewIsShown = false;
});
}
@ -407,41 +398,32 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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<CameraPreviewView> {
}
Future<void> 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<CameraPreviewView> {
}
setState(() {
sharePreviewIsShown = true;
_sharePreviewIsShown = true;
});
try {
@ -509,15 +491,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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<CameraPreviewView> {
onPanUpdate: onPanUpdate,
child: Stack(
children: [
if (galleryLoadedImageIsShown)
if (_galleryLoadedImageIsShown)
Center(
child: SizedBox(
width: 20,
@ -551,11 +533,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
),
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<CameraPreviewView> {
await widget.selectCamera(
(widget.selectedCameraDetails.cameraId + 1) % 2,
false,
false,
);
},
),
@ -598,7 +579,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
setState(() {});
},
),
if (!hasAudioPermission)
if (!_hasAudioPermission)
ActionButton(
Icons.mic_off_rounded,
color: Colors.white.withAlpha(160),
@ -606,27 +587,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
'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<CameraPreviewView> {
if (widget.cameraController!.value.isInitialized &&
widget.selectedCameraDetails.isZoomAble &&
!isFront &&
!isVideoRecording)
!_isVideoRecording)
SizedBox(
width: 120,
child: CameraZoomButtons(
@ -655,7 +621,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isVideoRecording)
if (!_isVideoRecording)
GestureDetector(
onTap: pickImageFromGallery,
child: Align(
@ -687,7 +653,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
shape: BoxShape.circle,
border: Border.all(
width: 7,
color: isVideoRecording
color: _isVideoRecording
? Colors.red
: Colors.white,
),
@ -695,7 +661,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
),
if (!isVideoRecording) const SizedBox(width: 80),
if (!_isVideoRecording) const SizedBox(width: 80),
],
),
],
@ -703,10 +669,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
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<CameraPreviewView> {
},
),
),
if (showSelfieFlash)
if (_showSelfieFlash)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(22),

View file

@ -22,7 +22,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
@override
void initState() {
super.initState();
unawaited(selectCamera(0, true, false));
unawaited(selectCamera(0, true));
}
@override
@ -36,13 +36,11 @@ class CameraSendToViewState extends State<CameraSendToView> {
Future<CameraController?> 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<CameraSendToView> {
}
await cameraController!.dispose();
cameraController = null;
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false);
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
@override

View file

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

View file

@ -116,6 +116,7 @@ class _FilterLayerState extends State<FilterLayer> {
}
},
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);

View file

@ -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,
),
],

View file

@ -211,6 +211,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
},
),
),
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<ShareImageEditorView> {
}
Future<void> 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<ShareImageEditorView> {
}
}
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<bool> 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<ShareImageEditorView> {
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;

View file

@ -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<ShareImageView> {
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,

View file

@ -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,
),

View file

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

View file

@ -70,7 +70,7 @@ class HomeViewState extends State<HomeView> {
}
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<HomeView> {
.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<HomeView> {
super.dispose();
}
Future<CameraController?> selectCamera(
int sCameraId,
bool init,
bool enableAudio,
) async {
Future<CameraController?> 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<HomeView> {
}
await cameraController!.dispose();
cameraController = null;
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false);
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
Future<void> initAsync() async {

View file

@ -357,7 +357,7 @@ class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
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,

View file

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

View file

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