starting with rewriting the upload process

This commit is contained in:
otsmr 2025-10-23 00:35:28 +02:00
parent f5cbcf154b
commit b2dc384465
36 changed files with 557 additions and 648 deletions

View file

@ -6,7 +6,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart';

View file

@ -1,5 +1,6 @@
import 'package:camera/camera.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart';
late ApiService apiService;
@ -9,6 +10,10 @@ late TwonlyDB twonlyDB;
List<CameraDescription> gCameras = <CameraDescription>[];
// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called,
// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart
late UserData gUser;
// The following global function can be called from anywhere to update
// the UI when something changed. The callbacks will be set by
// App widget.

View file

@ -10,8 +10,8 @@ import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
@ -28,6 +28,7 @@ void main() async {
final user = await getUser();
if (user != null) {
gUser = user;
if (user.isDemoUser) {
await deleteLocalUserData();
}

View file

@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
part 'messages.dao.g.dart';

View file

@ -10,10 +10,21 @@ enum MediaType {
}
enum UploadState {
pending,
readyToUpload,
uploadTaskStarted,
receiverNotified,
// Image/Video was taken. A database entry was created to track it...
initialized,
// Image was stored but not send
storedOnly,
// At this point the user is finished with editing, and the media file can be uploaded
compressing,
encrypting,
uploading,
backgroundUploadTaskStarted,
uploaded,
uploadLimitReached,
// readyToUpload,
// uploadTaskStarted,
// receiverNotified,
}
enum DownloadState {
@ -33,7 +44,8 @@ class MediaFiles extends Table {
TextColumn get uploadState => textEnum<UploadState>().nullable()();
TextColumn get downloadState => textEnum<DownloadState>().nullable()();
BoolColumn get requiresAuthentication => boolean()();
BoolColumn get requiresAuthentication =>
boolean().withDefault(const Constant(false))();
BoolColumn get reopenByContact =>
boolean().withDefault(const Constant(false))();

View file

@ -18,6 +18,8 @@ class Messages extends Table {
TextColumn get mediaId =>
text().nullable().references(MediaFiles, #mediaId)();
BlobColumn get downloadToken => blob().nullable()();
TextColumn get quotesMessageId =>
text().nullable().references(Messages, #messageId)();

View file

@ -1460,9 +1460,10 @@ class $MediaFilesTable extends MediaFiles
late final GeneratedColumn<bool> requiresAuthentication =
GeneratedColumn<bool>('requires_authentication', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: true,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("requires_authentication" IN (0, 1))'));
'CHECK ("requires_authentication" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _reopenByContactMeta =
const VerificationMeta('reopenByContact');
@override
@ -1563,8 +1564,6 @@ class $MediaFilesTable extends MediaFiles
_requiresAuthenticationMeta,
requiresAuthentication.isAcceptableOrUnknown(
data['requires_authentication']!, _requiresAuthenticationMeta));
} else if (isInserting) {
context.missing(_requiresAuthenticationMeta);
}
if (data.containsKey('reopen_by_contact')) {
context.handle(
@ -2017,7 +2016,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
required MediaType type,
this.uploadState = const Value.absent(),
this.downloadState = const Value.absent(),
required bool requiresAuthentication,
this.requiresAuthentication = const Value.absent(),
this.reopenByContact = const Value.absent(),
this.stored = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(),
@ -2028,8 +2027,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionNonce = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
}) : type = Value(type),
requiresAuthentication = Value(requiresAuthentication);
}) : type = Value(type);
static Insertable<MediaFile> custom({
Expression<String>? mediaId,
Expression<String>? type,
@ -2233,6 +2231,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES media_files (media_id)'));
static const VerificationMeta _downloadTokenMeta =
const VerificationMeta('downloadToken');
@override
late final GeneratedColumn<Uint8List> downloadToken =
GeneratedColumn<Uint8List>('download_token', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
static const VerificationMeta _quotesMessageIdMeta =
const VerificationMeta('quotesMessageId');
@override
@ -2319,6 +2323,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
senderId,
content,
mediaId,
downloadToken,
quotesMessageId,
isDeletedFromSender,
isEdited,
@ -2361,6 +2366,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
context.handle(_mediaIdMeta,
mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta));
}
if (data.containsKey('download_token')) {
context.handle(
_downloadTokenMeta,
downloadToken.isAcceptableOrUnknown(
data['download_token']!, _downloadTokenMeta));
}
if (data.containsKey('quotes_message_id')) {
context.handle(
_quotesMessageIdMeta,
@ -2428,6 +2439,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.string, data['${effectivePrefix}content']),
mediaId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}media_id']),
downloadToken: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}download_token']),
quotesMessageId: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}quotes_message_id']),
isDeletedFromSender: attachedDatabase.typeMapping.read(
@ -2461,6 +2474,7 @@ class Message extends DataClass implements Insertable<Message> {
final int? senderId;
final String? content;
final String? mediaId;
final Uint8List? downloadToken;
final String? quotesMessageId;
final bool isDeletedFromSender;
final bool isEdited;
@ -2476,6 +2490,7 @@ class Message extends DataClass implements Insertable<Message> {
this.senderId,
this.content,
this.mediaId,
this.downloadToken,
this.quotesMessageId,
required this.isDeletedFromSender,
required this.isEdited,
@ -2499,6 +2514,9 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || mediaId != null) {
map['media_id'] = Variable<String>(mediaId);
}
if (!nullToAbsent || downloadToken != null) {
map['download_token'] = Variable<Uint8List>(downloadToken);
}
if (!nullToAbsent || quotesMessageId != null) {
map['quotes_message_id'] = Variable<String>(quotesMessageId);
}
@ -2530,6 +2548,9 @@ class Message extends DataClass implements Insertable<Message> {
mediaId: mediaId == null && nullToAbsent
? const Value.absent()
: Value(mediaId),
downloadToken: downloadToken == null && nullToAbsent
? const Value.absent()
: Value(downloadToken),
quotesMessageId: quotesMessageId == null && nullToAbsent
? const Value.absent()
: Value(quotesMessageId),
@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId: serializer.fromJson<int?>(json['senderId']),
content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']),
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']),
isDeletedFromSender:
serializer.fromJson<bool>(json['isDeletedFromSender']),
@ -2578,6 +2600,7 @@ class Message extends DataClass implements Insertable<Message> {
'senderId': serializer.toJson<int?>(senderId),
'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId),
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
'quotesMessageId': serializer.toJson<String?>(quotesMessageId),
'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender),
'isEdited': serializer.toJson<bool>(isEdited),
@ -2596,6 +2619,7 @@ class Message extends DataClass implements Insertable<Message> {
Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
bool? isDeletedFromSender,
bool? isEdited,
@ -2611,6 +2635,8 @@ class Message extends DataClass implements Insertable<Message> {
senderId: senderId.present ? senderId.value : this.senderId,
content: content.present ? content.value : this.content,
mediaId: mediaId.present ? mediaId.value : this.mediaId,
downloadToken:
downloadToken.present ? downloadToken.value : this.downloadToken,
quotesMessageId: quotesMessageId.present
? quotesMessageId.value
: this.quotesMessageId,
@ -2630,6 +2656,9 @@ class Message extends DataClass implements Insertable<Message> {
senderId: data.senderId.present ? data.senderId.value : this.senderId,
content: data.content.present ? data.content.value : this.content,
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
downloadToken: data.downloadToken.present
? data.downloadToken.value
: this.downloadToken,
quotesMessageId: data.quotesMessageId.present
? data.quotesMessageId.value
: this.quotesMessageId,
@ -2658,6 +2687,7 @@ class Message extends DataClass implements Insertable<Message> {
..write('senderId: $senderId, ')
..write('content: $content, ')
..write('mediaId: $mediaId, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ')
..write('isEdited: $isEdited, ')
@ -2678,6 +2708,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId,
content,
mediaId,
$driftBlobEquality.hash(downloadToken),
quotesMessageId,
isDeletedFromSender,
isEdited,
@ -2696,6 +2727,7 @@ class Message extends DataClass implements Insertable<Message> {
other.senderId == this.senderId &&
other.content == this.content &&
other.mediaId == this.mediaId &&
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
other.quotesMessageId == this.quotesMessageId &&
other.isDeletedFromSender == this.isDeletedFromSender &&
other.isEdited == this.isEdited &&
@ -2713,6 +2745,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<int?> senderId;
final Value<String?> content;
final Value<String?> mediaId;
final Value<Uint8List?> downloadToken;
final Value<String?> quotesMessageId;
final Value<bool> isDeletedFromSender;
final Value<bool> isEdited;
@ -2729,6 +2762,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(),
this.content = const Value.absent(),
this.mediaId = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(),
this.isEdited = const Value.absent(),
@ -2746,6 +2780,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(),
this.content = const Value.absent(),
this.mediaId = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(),
this.isEdited = const Value.absent(),
@ -2763,6 +2798,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Expression<int>? senderId,
Expression<String>? content,
Expression<String>? mediaId,
Expression<Uint8List>? downloadToken,
Expression<String>? quotesMessageId,
Expression<bool>? isDeletedFromSender,
Expression<bool>? isEdited,
@ -2780,6 +2816,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (senderId != null) 'sender_id': senderId,
if (content != null) 'content': content,
if (mediaId != null) 'media_id': mediaId,
if (downloadToken != null) 'download_token': downloadToken,
if (quotesMessageId != null) 'quotes_message_id': quotesMessageId,
if (isDeletedFromSender != null)
'is_deleted_from_sender': isDeletedFromSender,
@ -2800,6 +2837,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Value<int?>? senderId,
Value<String?>? content,
Value<String?>? mediaId,
Value<Uint8List?>? downloadToken,
Value<String?>? quotesMessageId,
Value<bool>? isDeletedFromSender,
Value<bool>? isEdited,
@ -2816,6 +2854,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
senderId: senderId ?? this.senderId,
content: content ?? this.content,
mediaId: mediaId ?? this.mediaId,
downloadToken: downloadToken ?? this.downloadToken,
quotesMessageId: quotesMessageId ?? this.quotesMessageId,
isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender,
isEdited: isEdited ?? this.isEdited,
@ -2847,6 +2886,9 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (mediaId.present) {
map['media_id'] = Variable<String>(mediaId.value);
}
if (downloadToken.present) {
map['download_token'] = Variable<Uint8List>(downloadToken.value);
}
if (quotesMessageId.present) {
map['quotes_message_id'] = Variable<String>(quotesMessageId.value);
}
@ -2888,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('senderId: $senderId, ')
..write('content: $content, ')
..write('mediaId: $mediaId, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ')
..write('isEdited: $isEdited, ')
@ -7100,7 +7143,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
required MediaType type,
Value<UploadState?> uploadState,
Value<DownloadState?> downloadState,
required bool requiresAuthentication,
Value<bool> requiresAuthentication,
Value<bool> reopenByContact,
Value<bool> stored,
Value<List<int>?> reuploadRequestedBy,
@ -7433,7 +7476,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
required MediaType type,
Value<UploadState?> uploadState = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(),
required bool requiresAuthentication,
Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
@ -7513,6 +7556,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId,
Value<String?> content,
Value<String?> mediaId,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender,
Value<bool> isEdited,
@ -7530,6 +7574,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId,
Value<String?> content,
Value<String?> mediaId,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender,
Value<bool> isEdited,
@ -7671,6 +7716,9 @@ class $$MessagesTableFilterComposer
ColumnFilters<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender,
builder: (column) => ColumnFilters(column));
@ -7856,6 +7904,10 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender,
builder: (column) => ColumnOrderings(column));
@ -7978,6 +8030,9 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => column);
GeneratedColumn<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender, builder: (column) => column);
@ -8181,6 +8236,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(),
Value<bool> isEdited = const Value.absent(),
@ -8198,6 +8254,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId,
content: content,
mediaId: mediaId,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender,
isEdited: isEdited,
@ -8215,6 +8272,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(),
Value<bool> isEdited = const Value.absent(),
@ -8232,6 +8290,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId,
content: content,
mediaId: mediaId,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender,
isEdited: isEdited,

View file

@ -5,8 +5,8 @@ import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
class MemoryItem {
MemoryItem({

View file

@ -23,8 +23,8 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart';

View file

@ -12,10 +12,10 @@ 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/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {

View file

@ -0,0 +1,50 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> initFileDownloader() async {
FileDownloader().updates.listen((update) async {
switch (update) {
case TaskStatusUpdate():
if (update.task.taskId.contains('upload_')) {
await handleUploadStatusUpdate(update);
}
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update);
}
case TaskProgressUpdate():
Log.info(
'Progress update for ${update.task} with progress ${update.progress}',
);
}
});
await FileDownloader().start();
try {
var androidConfig = [];
if (kDebugMode) {
androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)];
}
await FileDownloader().configure(androidConfig: androidConfig);
} catch (e) {
Log.error(e);
}
if (kDebugMode) {
FileDownloader().configureNotification(
running: const TaskNotification(
'Uploading/Downloading',
'{filename} ({progress}).',
),
progressBar: true,
);
}
}

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
@ -17,14 +16,13 @@ import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/media_uploads_table.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
@ -33,76 +31,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> isAllowedToSend() async {
final user = await getUser();
if (user == null) return null;
if (user.subscriptionPlan == 'Free') {
var todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 10) {
return ErrorCode.PlanLimitReached;
}
todaysImageCounter = user.todaysImageCounter! + 1;
} else {
todaysImageCounter = 1;
}
} else {
todaysImageCounter = 1;
}
await updateUserdata((user) {
user
..lastImageSend = DateTime.now()
..todaysImageCounter = todaysImageCounter;
return user;
});
}
return null;
}
Future<void> initFileDownloader() async {
FileDownloader().updates.listen((update) async {
switch (update) {
case TaskStatusUpdate():
if (update.task.taskId.contains('upload_')) {
await handleUploadStatusUpdate(update);
}
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update);
}
case TaskProgressUpdate():
Log.info(
'Progress update for ${update.task} with progress ${update.progress}',
);
}
});
await FileDownloader().start();
try {
var androidConfig = [];
if (kDebugMode) {
androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)];
}
await FileDownloader().configure(androidConfig: androidConfig);
} catch (e) {
Log.error(e);
}
if (kDebugMode) {
FileDownloader().configureNotification(
running: const TaskNotification(
'Uploading/Downloading',
'{filename} ({progress}).',
),
progressBar: true,
);
}
}
/// States:
/// when user recorded an video
/// 1. Compress video
@ -115,43 +43,43 @@ Future<void> initFileDownloader() async {
/// Create a new entry in the database
Future<bool> checkForFailedUploads() async {
final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
final mediaUploadIds = <int>[];
for (final message in messages) {
if (mediaUploadIds.contains(message.mediaUploadId)) {
continue;
}
final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
message.mediaUploadId!,
const MediaUploadsCompanion(
state: Value(UploadState.pending),
encryptionData: Value(
null, // start from scratch e.q. encrypt the files again if already happen
),
),
);
if (affectedRows == 0) {
Log.error(
'The media from message ${message.messageId} already deleted.',
);
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
const MessagesCompanion(
errorWhileSending: Value(true),
),
);
} else {
mediaUploadIds.add(message.mediaUploadId!);
}
}
if (messages.isNotEmpty) {
Log.error(
'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
);
}
return mediaUploadIds.isNotEmpty; // return true if there are affected
}
// Future<bool> checkForFailedUploads() async {
// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
// final mediaUploadIds = <int>[];
// for (final message in messages) {
// if (mediaUploadIds.contains(message.mediaUploadId)) {
// continue;
// }
// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
// message.mediaUploadId!,
// const MediaUploadsCompanion(
// state: Value(UploadState.pending),
// encryptionData: Value(
// null, // start from scratch e.q. encrypt the files again if already happen
// ),
// ),
// );
// if (affectedRows == 0) {
// Log.error(
// 'The media from message ${message.messageId} already deleted.',
// );
// await twonlyDB.messagesDao.updateMessageByMessageId(
// message.messageId,
// const MessagesCompanion(
// errorWhileSending: Value(true),
// ),
// );
// } else {
// mediaUploadIds.add(message.mediaUploadId!);
// }
// }
// if (messages.isNotEmpty) {
// Log.error(
// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
// );
// }
// return mediaUploadIds.isNotEmpty; // return true if there are affected
// }
final lockingHandleMediaFile = Mutex();
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
@ -192,82 +120,25 @@ Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
}
}
Future<int?> initMediaUpload() async {
return twonlyDB.mediaUploadsDao
.insertMediaUpload(const MediaUploadsCompanion());
}
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
await videoFilePath.copy('$basePath.original.mp4');
return compressVideoIfExists(mediaUploadId);
}
Future<Uint8List> addOrModifyImageToUpload(
int mediaUploadId,
Uint8List imageBytes,
Future<MediaFileService?> initializeMediaUpload(
MediaType type,
int? displayLimitInMilliseconds,
) async {
Uint8List imageBytesCompressed;
final chacha20 = FlutterChacha20.poly1305Aead();
final encryptionKey = await (await chacha20.newSecretKey()).extract();
final encryptionNonce = chacha20.newNonce();
final stopwatch = Stopwatch()..start();
Log.info('Raw images size in bytes: ${imageBytes.length}');
try {
imageBytesCompressed = await FlutterImageCompress.compressWithList(
format: CompressFormat.webp,
// minHeight: 0,
// minWidth: 0,
imageBytes,
quality: 90,
);
if (imageBytesCompressed.length >= 1 * 1000 * 1000) {
// if the media file is over 2MB compress it with 60%
imageBytesCompressed = await FlutterImageCompress.compressWithList(
format: CompressFormat.webp,
imageBytes,
quality: 60,
);
}
await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed);
} catch (e) {
Log.error('$e');
// as a fall back use the original image
await writeSendMediaFile(mediaUploadId, 'png', imageBytes);
imageBytesCompressed = imageBytes;
}
stopwatch.stop();
Log.info(
'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds',
);
Log.info('Raw images size in bytes: ${imageBytesCompressed.length}');
// stopwatch.reset();
// stopwatch.start();
// // var helper = MediaUploadHelper();
// try {
// final webpBytes =
// await convertAndCompressImage(pngRawImageBytes: imageBytes);
// Log.info(
// 'Compression the image in rust took: ${stopwatch.elapsedMilliseconds} milliseconds');
// Log.info("Raw images size in bytes using webp: ${webpBytes.length}");
// } catch (e) {
// Log.error("$e");
// }
/// in case the media file was already encrypted of even uploaded
/// remove the data so it will be done again.
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
const MediaUploadsCompanion(
encryptionData: Value(null),
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
uploadState: const Value(UploadState.initialized),
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)),
encryptionNonce: Value(Uint8List.fromList(encryptionNonce)),
type: Value(type),
),
);
return imageBytesCompressed;
if (mediaFile == null) return null;
return MediaFileService.fromMedia(mediaFile);
}
Future<void> handlePreProcessingState(MediaUpload media) async {
@ -304,7 +175,6 @@ Future<void> encryptMediaFiles(
final state = MediaEncryptionData();
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKey = await (await chacha20.newSecretKey()).extract();
state
..encryptionKey = secretKey.bytes
@ -338,70 +208,37 @@ Future<void> encryptMediaFiles(
}
Future<void> finalizeUpload(
int mediaUploadId,
List<int> contactIds,
bool isRealTwonly,
bool isVideo,
bool mirrorVideo,
int maxShowTime,
MediaFileService mediaService,
List<String> groupIds,
) async {
final metadata = MediaUploadMetadata()
..contactIds = contactIds
..isRealTwonly = isRealTwonly
..messageSendAt = DateTime.now()
..isVideo = isVideo
..maxShowTime = maxShowTime
..mirrorVideo = mirrorVideo;
final messageIds = <Message>[];
final messageIds = <int>[];
for (final contactId in contactIds) {
final messageId = await twonlyDB.messagesDao.insertMessage(
for (final groupId in groupIds) {
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(contactId),
kind: const Value(MessageKind.media),
sendAt: Value(metadata.messageSendAt),
downloadState: const Value(DownloadState.pending),
mediaUploadId: Value(mediaUploadId),
contentJson: Value(
jsonEncode(
MediaMessageContent(
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
isVideo: isVideo,
mirrorVideo: mirrorVideo,
).toJson(),
),
groupId: Value(groupId),
mediaId: Value(mediaService.mediaFile.mediaId),
),
);
if (message != null) {
messageIds.add(message);
// de-archive contact when sending a new message
await twonlyDB.groupsDao.updateGroup(
message.groupId,
const GroupsCompanion(
archived: Value(false),
),
),
);
// de-archive contact when sending a new message
await twonlyDB.contactsDao.updateContact(
contactId,
const ContactsCompanion(
archived: Value(false),
),
);
if (messageId != null) {
messageIds.add(messageId);
);
} else {
Log.error('Error inserting media upload message in database.');
}
}
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
MediaUploadsCompanion(
messageIds: Value(messageIds),
metadata: Value(metadata),
),
);
unawaited(handleNextMediaUploadSteps(mediaUploadId));
unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId));
}
final lockingHandleNextMediaUploadStep = Mutex();
Future<void> handleNextMediaUploadSteps(int mediaUploadId) async {
Future<void> handleNextMediaUploadSteps(String mediaUploadId) async {
await lockingHandleNextMediaUploadStep.protect(() async {
final mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
@ -549,7 +386,7 @@ Future<void> handleMediaUpload(MediaUpload media) async {
continue;
}
final downloadToken = createDownloadToken();
final downloadToken = getRandomUint8List(32);
final msg = MessageJson(
kind: MessageKind.media,
@ -734,159 +571,14 @@ Future<void> uploadFileFast(
Log.info('Upload successful!');
await handleUploadSuccess(media);
return;
} else if (response.statusCode == 429) {
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploadLimitReached),
),
);
} else {
Log.info('Upload failed with status: ${response.statusCode}');
}
}
Future<bool> compressVideoIfExists(int mediaUploadId) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final videoOriginalFile = File('$basePath.original.mp4');
final videoCompressedFile = File('$basePath.mp4');
if (videoCompressedFile.existsSync()) {
// file is already compressed and exists
return true;
}
if (!videoOriginalFile.existsSync()) {
// media upload does not have a video
return false;
}
final stopwatch = Stopwatch()..start();
MediaInfo? mediaInfo;
try {
mediaInfo = await VideoCompress.compressVideo(
videoOriginalFile.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(
videoOriginalFile.path,
quality: VideoQuality.Res960x540Quality,
includeAudio: true,
);
}
} catch (e) {
Log.error('during video compression: $e');
}
stopwatch.stop();
Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video');
if (mediaInfo == null) {
Log.error('could not compress video.');
// as a fall back use the non compressed version
await videoOriginalFile.copy(videoCompressedFile.path);
await videoOriginalFile.delete();
} else {
await mediaInfo.file!.copy(videoCompressedFile.path);
await mediaInfo.file!.delete();
}
return true;
}
/// --- helper functions ---
Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
if (!file.existsSync()) {
throw Exception('$file not found');
}
return file.readAsBytes();
}
Future<File> writeSendMediaFile(
int mediaUploadId,
String type,
Uint8List data,
) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
await file.writeAsBytes(data);
return file;
}
Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
if (file.existsSync()) {
await file.delete();
}
}
Future<String> getMediaFilePath(dynamic mediaId, String type) async {
final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true);
}
return join(mediaSendDir.path, '$mediaId');
}
Future<String> getMediaBaseFilePath(String type) async {
final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true);
}
return mediaSendDir.path;
}
/// combines two utf8 list
Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) {
final combinedLength = 4 + list1.length + list2.length;
final combinedList = Uint8List(combinedLength);
ByteData.sublistView(combinedList).setInt32(0, list1.length);
combinedList
..setRange(4, 4 + list1.length, list1)
..setRange(4 + list1.length, combinedLength, list2);
return combinedList;
}
List<Uint8List> extractUint8Lists(Uint8List combinedList) {
final byteData = ByteData.sublistView(combinedList);
final sizeOfList1 = byteData.getInt32(0);
final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1);
final list2 = Uint8List.view(
combinedList.buffer,
4 + sizeOfList1,
combinedList.lengthInBytes - 4 - sizeOfList1,
);
return [list1, list2];
}
Future<void> purgeSendMediaFiles() async {
final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', 'send'));
await purgeMediaFiles(directory);
}
String uint8ListToHex(List<int> bytes) {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
List<int>.generate(
hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);
Uint8List createDownloadToken() {
final random = Random();
final token = Uint8List(32);
for (var j = 0; j < 32; j++) {
token[j] = random.nextInt(256); // Generate a random byte (0-255)
}
return token;
}

View file

@ -4,9 +4,9 @@ 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/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleMedia(
@ -157,7 +157,7 @@ Future<void> handleMediaUpdate(
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.pending),
uploadState: const Value(UploadState.uploading),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);

View file

@ -57,13 +57,7 @@ ClientToServer createClientToServerFromApplicationData(
return ClientToServer()..v0 = v0;
}
Future<void> deleteContact(int contactId) async {
await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
}
Future<void> rejectUser(int contactId) async {
Future<void> rejectAndDeleteContact(int contactId) async {
await sendCipherText(
contactId,
EncryptedContent(
@ -72,6 +66,9 @@ Future<void> rejectUser(int contactId) async {
),
),
);
await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
}
Future<void> handleMediaError(MediaFile media) async {

View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:video_compress/video_compress.dart';
Future<void> compressImage(
File sourceFile,
File destinationFile,
) async {
final stopwatch = Stopwatch()..start();
try {
var compressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path,
format: CompressFormat.webp,
quality: 90,
);
if (compressedBytes == null) {
throw Exception(
'Could not compress media file: $sourceFile. Sending original file.',
);
}
Log.info('Compressed images size in bytes: ${compressedBytes.length}');
if (compressedBytes.length >= 1 * 1000 * 1000) {
// if the media file is over 1MB compress it with 60%
final tmpCompressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path,
format: CompressFormat.webp,
quality: 60,
);
if (tmpCompressedBytes != null) {
Log.error(
'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.',
);
compressedBytes = tmpCompressedBytes;
}
}
await destinationFile.writeAsBytes(compressedBytes);
} catch (e) {
Log.error('$e');
sourceFile.copySync(destinationFile.path);
}
stopwatch.stop();
Log.info(
'Compression of the image took: ${stopwatch.elapsedMilliseconds} milliseconds.',
);
}
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');
}
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);
} else {
await mediaInfo.file!.copy(destinationFile.path);
}
}

View file

@ -1,12 +1,12 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.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/thumbnail.service.dart';
import 'package:twonly/src/services/mediafiles/compression.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart';
class MediaFileService {
@ -39,6 +39,28 @@ class MediaFileService {
}
}
Future<void> setDisplayLimit(int? displayLimitInMilliseconds) async {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
),
);
await updateFromDB();
}
Future<void> setRequiresAuth(bool requiresAuthentication) async {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
requiresAuthentication: Value(requiresAuthentication),
displayLimitInMilliseconds:
requiresAuthentication ? const Value(12) : const Value.absent(),
),
);
await updateFromDB();
}
Future<void> createThumbnail() async {
if (!storedPath.existsSync()) {
Log.error('Could not create Thumbnail as stored media does not exists.');
@ -54,18 +76,35 @@ class MediaFileService {
}
}
Future<void> compressMedia() async {
if (!originalPath.existsSync()) {
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);
case MediaType.gif:
originalPath.renameSync(tempPath.path);
Log.error('Compression for .gif is not implemented yet.');
}
}
void fullMediaRemoval() {
if (tempPath.existsSync()) {
tempPath.deleteSync();
}
if (encryptedPath.existsSync()) {
encryptedPath.deleteSync();
}
if (storedPath.existsSync()) {
storedPath.deleteSync();
}
if (thumbnailPath.existsSync()) {
thumbnailPath.deleteSync();
final pathsToRemove = [
tempPath,
encryptedPath,
originalPath,
storedPath,
thumbnailPath
];
for (final path in pathsToRemove) {
if (path.existsSync()) {
path.deleteSync();
}
}
}
@ -121,4 +160,8 @@ class MediaFileService {
'tmp',
namePrefix: '.encrypted',
);
File get originalPath => _buildFilePath(
'tmp',
namePrefix: '.original',
);
}

View file

@ -4,7 +4,7 @@ import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';

View file

@ -14,7 +14,7 @@ import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';

View file

@ -1,6 +1,4 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -11,14 +9,13 @@ import 'package:local_auth/local_auth.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart';
extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!;
TwonlyDatabase get db => Provider.of<TwonlyDatabase>(this);
TwonlyDB get db => Provider.of<TwonlyDB>(this);
ColorScheme get color => Theme.of(this).colorScheme;
}
@ -245,31 +242,19 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}';
}
String getMessageText(Message message) {
try {
if (message.contentJson == null) return '';
return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map)
.text;
} catch (e) {
Log.error(e);
return '';
}
}
MediaMessageContent? getMediaContent(Message message) {
try {
if (message.contentJson == null) return null;
return MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map,
);
} catch (e) {
Log.error(e);
return null;
}
}
bool isUUIDNewer(String uuid1, String uuid2) {
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
return timestamp1 > timestamp2;
}
String uint8ListToHex(List<int> bytes) {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
List<int>.generate(
hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);

View file

@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart';
@ -14,6 +15,7 @@ Future<bool> isUserCreated() async {
if (user == null) {
return false;
}
gUser = user;
return true;
}
@ -56,7 +58,8 @@ Future<UserData?> updateUserdata(
final updated = updateUser(user);
await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
return user;
gUser = updated;
return updated;
});
}

View file

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path/path.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';

View file

@ -10,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
@ -299,17 +301,35 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
File? videoFilePath, {
bool sharedFromGallery = false,
}) async {
final mediaFileService = await initializeMediaUpload(
(videoFilePath != null) ? MediaType.video : MediaType.image,
gUser.defaultShowTime,
);
if (!mounted) return true;
if (mediaFileService == null) {
Log.error('Could not generate media file service');
return false;
}
if (videoFilePath != null) {
videoFilePath
..copySync(mediaFileService.originalPath.path)
..deleteSync();
// Start with compressing the video, to speed up the process in case the video is not changed.
unawaited(mediaFileService.compressMedia());
}
final shouldReturn = await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) => ShareImageEditorView(
videoFilePath: videoFilePath,
imageBytes: imageBytes,
imageBytesFuture: imageBytes,
sharedFromGallery: sharedFromGallery,
sendTo: widget.sendTo,
mirrorVideo: isFront && Platform.isAndroid && false,
useHighQuality: true,
mediaFileService: mediaFileService,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child;

View file

@ -3,14 +3,19 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
@ -34,32 +39,26 @@ const gMediaShowInfinite = 999999;
class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({
required this.mirrorVideo,
required this.useHighQuality,
required this.sharedFromGallery,
required this.mediaFileService,
super.key,
this.imageBytes,
this.imageBytesFuture,
this.sendTo,
this.videoFilePath,
});
final Future<Uint8List?>? imageBytes;
final File? videoFilePath;
final Contact? sendTo;
final bool mirrorVideo;
final bool useHighQuality;
final Future<Uint8List?>? imageBytesFuture;
final Group? sendTo;
final bool sharedFromGallery;
final MediaFileService mediaFileService;
@override
State<ShareImageEditorView> createState() => _ShareImageEditorView();
}
class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _isRealTwonly = false;
int maxShowTime = gMediaShowInfinite;
double tabDownPosition = 0;
bool sendingOrLoadingImage = true;
bool loadingImage = true;
bool isDisposed = false;
HashSet<int> selectedUserIds = HashSet();
HashSet<String> selectedGroupIds = HashSet();
double widthRatio = 1;
double heightRatio = 1;
double pixelRatio = 1;
@ -68,26 +67,31 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ScreenshotController screenshotController = ScreenshotController();
/// Media upload variables
int? mediaUploadId;
Future<bool>? videoUploadHandler;
MediaFileService get mediaService => widget.mediaFileService;
MediaFile get media => widget.mediaFileService.mediaFile;
@override
void initState() {
super.initState();
unawaited(initAsync());
unawaited(initMediaFileUpload());
layers.add(FilterLayerData());
if (widget.sendTo != null) {
selectedUserIds.add(widget.sendTo!.userId);
selectedGroupIds.add(widget.sendTo!.groupId);
}
if (widget.imageBytes != null) {
unawaited(loadImage(widget.imageBytes!));
} else if (widget.videoFilePath != null) {
if (widget.imageBytesFuture != null) {
unawaited(loadImage(widget.imageBytesFuture!));
}
if (media.type == MediaType.video) {
setState(() {
sendingOrLoadingImage = false;
loadingImage = false;
});
videoController = VideoPlayerController.file(widget.videoFilePath!);
videoController = VideoPlayerController.file(mediaService.originalPath);
videoController?.setLooping(true);
videoController?.initialize().then((_) async {
await videoController!.play();
@ -97,29 +101,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
}
Future<void> initAsync() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime != null) {
setState(() {
maxShowTime = user.defaultShowTime!;
});
}
}
Future<void> initMediaFileUpload() async {
// media init was already called...
if (mediaUploadId != null) return;
mediaUploadId = await initMediaUpload();
if (widget.videoFilePath != null && mediaUploadId != null) {
// start with the video compression...
videoUploadHandler =
addVideoToUpload(mediaUploadId!, widget.videoFilePath!);
}
}
@override
void dispose() {
isDisposed = true;
@ -128,14 +109,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.dispose();
}
void updateStatus(int userId, bool checked) {
void updateSelectedGroupIds(String groupId, bool checked) {
if (checked) {
if (_isRealTwonly) {
selectedUserIds.clear();
if (media.requiresAuthentication) {
selectedGroupIds.clear();
}
selectedUserIds.add(userId);
selectedGroupIds.add(groupId);
} else {
selectedUserIds.remove(userId);
selectedGroupIds.remove(groupId);
}
setState(() {});
}
@ -195,38 +176,36 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
),
const SizedBox(height: 8),
NotificationBadge(
count: (widget.videoFilePath != null)
count: (media.type != MediaType.video)
? '0'
: maxShowTime == gMediaShowInfinite
: media.displayLimitInMilliseconds == null
? ''
: maxShowTime.toString(),
: media.displayLimitInMilliseconds.toString(),
child: ActionButton(
(widget.videoFilePath != null)
? maxShowTime == gMediaShowInfinite
(media.type != MediaType.video)
? media.displayLimitInMilliseconds == null
? Icons.repeat_rounded
: Icons.repeat_one_rounded
: Icons.timer_outlined,
tooltipText: context.lang.protectAsARealTwonly,
onPressed: () async {
if (widget.videoFilePath != null) {
setState(() {
if (maxShowTime == gMediaShowInfinite) {
maxShowTime = 0;
} else {
maxShowTime = gMediaShowInfinite;
}
});
if (media.type != MediaType.video) {
await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null);
if (!mounted) return;
setState(() {});
return;
}
if (maxShowTime == gMediaShowInfinite) {
int? maxShowTime;
if (media.displayLimitInMilliseconds == null) {
maxShowTime = 1;
} else if (maxShowTime == 1) {
} else if (media.displayLimitInMilliseconds == 1) {
maxShowTime = 5;
} else if (maxShowTime == 5) {
} else if (media.displayLimitInMilliseconds == 5) {
maxShowTime = 20;
} else {
maxShowTime = gMediaShowInfinite;
}
await mediaService.setDisplayLimit(maxShowTime);
if (!mounted) return;
setState(() {});
await updateUserdata((user) {
user.defaultShowTime = maxShowTime;
@ -239,15 +218,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ActionButton(
FontAwesomeIcons.shieldHeart,
tooltipText: context.lang.protectAsARealTwonly,
color: _isRealTwonly
color: media.requiresAuthentication
? Theme.of(context).colorScheme.primary
: Colors.white,
onPressed: () async {
_isRealTwonly = !_isRealTwonly;
if (_isRealTwonly) {
maxShowTime = 12;
}
selectedUserIds = HashSet();
await mediaService.setRequiresAuth(!media.requiresAuthentication);
selectedGroupIds = HashSet();
setState(() {});
},
),
@ -308,11 +284,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
Future<void> pushShareImageView() async {
if (mediaUploadId == null) {
await initMediaFileUpload();
if (mediaUploadId == null) return;
}
final imageBytes = getMergedImage();
final imageBytes = storeImageAsOriginal();
await videoController?.pause();
if (isDisposed || !mounted) return;
final wasSend = await Navigator.push(
@ -320,13 +292,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
MaterialPageRoute(
builder: (context) => ShareImageView(
imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly,
maxShowTime: maxShowTime,
selectedUserIds: selectedUserIds,
updateStatus: updateStatus,
selectedUserIds: selectedGroupIds,
updateStatus: updateSelectedGroupIds,
videoUploadHandler: videoUploadHandler,
mediaUploadId: mediaUploadId!,
mirrorVideo: widget.mirrorVideo,
mediaFileService: mediaService,
),
),
) as bool?;
@ -337,36 +306,46 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
}
Future<Uint8List?> getMergedImage() async {
Uint8List? image;
Future<void> storeImageAsOriginal() async {
if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes;
mediaService.originalPath.writeAsBytesSync(image);
}
}
TODO: When changed then create a new mediaID!!!!!!
As storedMediaId would overwrite it....
if (layers.length > 1 || widget.videoFilePath != null) {
if (layers.length > 1 || media.type != MediaType.video) {
for (final x in layers) {
x.showCustomButtons = false;
}
setState(() {});
image = await screenshotController.capture(
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1,
final image = await screenshotController.capture(
pixelRatio: pixelRatio,
);
if (image == null) {
Log.error('screenshotController did not return image bytes');
return;
}
mediaService.originalPath.writeAsBytesSync(image);
// In case the image was already stored, then rename the stored image.
if (mediaService.storedPath.existsSync()) {
final newPath = mediaService.storedPath.absolute.path
.replaceFirst(media.mediaId, uuid.v7());
mediaService.storedPath.renameSync(newPath);
}
for (final x in layers) {
x.showCustomButtons = true;
}
setState(() {});
} else if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
image = (layers.first as BackgroundLayerData).image.bytes;
}
}
return image;
}
Future<void> loadImage(Future<Uint8List?> imageFile) async {
final imageBytes = await imageFile;
await currentImage.load(imageBytes);
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
await currentImage.load(await imageBytesFuture);
if (isDisposed) return;
if (!context.mounted) return;
@ -388,56 +367,29 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {
sendingOrLoadingImage = true;
});
final imageBytes = await getMergedImage();
if (media.type == MediaType.image) {
await storeImageAsOriginal();
}
if (media.type == MediaType.video) {
Log.error('TODO: COMBINE VIDEO AND IMAGE!!!');
}
if (!context.mounted) return;
if (imageBytes == null) {
// first finalize the upload
await finalizeUpload(mediaService, [widget.sendTo!.groupId]);
/// then call the upload process in the background
await encryptMediaFiles(
mediaUploadId!,
imageHandler,
videoUploadHandler,
);
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context, true);
return;
}
final err = await isAllowedToSend();
if (!context.mounted) return;
if (err != null) {
setState(() {
sendingOrLoadingImage = false;
});
if (mounted) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return SubscriptionView(
redirectError: err,
);
},
),
);
}
} else {
final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes);
// first finalize the upload
await finalizeUpload(
mediaUploadId!,
[widget.sendTo!.userId],
_isRealTwonly,
widget.videoFilePath != null,
widget.mirrorVideo,
maxShowTime,
);
/// then call the upload process in the background
await encryptMediaFiles(
mediaUploadId!,
imageHandler,
videoUploadHandler,
);
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context, true);
}
}
}
@ -543,10 +495,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
children: [
if (videoController != null)
Positioned.fill(
child: Transform.flip(
flipX: widget.mirrorVideo,
child: VideoPlayer(videoController!),
),
child: VideoPlayer(videoController!),
),
Screenshot(
controller: screenshotController,

View file

@ -9,7 +9,8 @@ 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/twonly.db.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
import 'package:twonly/src/views/components/flame.dart';
@ -21,25 +22,19 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class ShareImageView extends StatefulWidget {
const ShareImageView({
required this.imageBytesFuture,
required this.isRealTwonly,
required this.mirrorVideo,
required this.maxShowTime,
required this.selectedUserIds,
required this.updateStatus,
required this.videoUploadHandler,
required this.mediaUploadId,
required this.mediaFileService,
super.key,
this.enableVideoAudio,
});
final Future<Uint8List?> imageBytesFuture;
final bool isRealTwonly;
final bool mirrorVideo;
final int maxShowTime;
final HashSet<int> selectedUserIds;
final bool? enableVideoAudio;
final int mediaUploadId;
final void Function(int, bool) updateStatus;
final Future<bool>? videoUploadHandler;
final MediaFileService mediaFileService;
@override
State<ShareImageView> createState() => _ShareImageView();

View file

@ -227,8 +227,7 @@ class ContactsListView extends StatelessWidget {
child: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async {
await rejectUser(contact.userId);
await deleteContact(contact.userId);
await rejectAndDeleteContact(contact.userId);
},
),
),

View file

@ -12,7 +12,7 @@ import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';

View file

@ -8,7 +8,8 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/services/api/media_download.dart' as received;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'
as received;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';

View file

@ -15,7 +15,7 @@ import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';

View file

@ -31,8 +31,7 @@ class _ContactViewState extends State<ContactView> {
);
if (remove) {
// trigger deletion for the other user...
await rejectUser(contact.userId);
await deleteContact(contact.userId);
await rejectAndDeleteContact(contact.userId);
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}

View file

@ -6,8 +6,8 @@ import 'package:intl/intl.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';

View file

@ -6,8 +6,9 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/media_download.dart' as received;
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'
as received;
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';

View file

@ -90,6 +90,8 @@ class _RegisterViewState extends State<RegisterView> {
await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
gUser = userData;
await apiService.authenticate();
widget.callbackOnSuccess();
}

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';

View file

@ -8,7 +8,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/services/api/media_upload.dart'
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'
show createDownloadToken, uint8ListToHex;
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';

View file

@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/views/components/animate_icon.dart';