mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
video sending and receiving works #25
This commit is contained in:
parent
5a6afaa6d4
commit
53ccb325b5
17 changed files with 3382 additions and 215 deletions
1
drift_schemas/twonly_database/drift_schema_v5.json
Normal file
1
drift_schemas/twonly_database/drift_schema_v5.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -65,6 +65,7 @@ class MediaUploadMetadata {
|
||||||
MediaUploadMetadata state = MediaUploadMetadata();
|
MediaUploadMetadata state = MediaUploadMetadata();
|
||||||
state.contactIds = List<int>.from(json['contactIds']);
|
state.contactIds = List<int>.from(json['contactIds']);
|
||||||
state.isRealTwonly = json['isRealTwonly'];
|
state.isRealTwonly = json['isRealTwonly'];
|
||||||
|
state.videoWithAudio = json['videoWithAudio'];
|
||||||
state.isVideo = json['isVideo'];
|
state.isVideo = json['isVideo'];
|
||||||
state.maxShowTime = json['maxShowTime'];
|
state.maxShowTime = json['maxShowTime'];
|
||||||
state.maxShowTime = json['maxShowTime'];
|
state.maxShowTime = json['maxShowTime'];
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
TwonlyDatabase.forTesting(DatabaseConnection super.connection);
|
TwonlyDatabase.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 4;
|
int get schemaVersion => 5;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
|
|
@ -57,19 +57,19 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
return MigrationStrategy(
|
return MigrationStrategy(
|
||||||
onUpgrade: stepByStep(
|
onUpgrade: stepByStep(from1To2: (m, schema) async {
|
||||||
from1To2: (m, schema) async {
|
m.addColumn(schema.messages, schema.messages.errorWhileSending);
|
||||||
m.addColumn(schema.messages, schema.messages.errorWhileSending);
|
}, from2To3: (m, schema) async {
|
||||||
},
|
m.addColumn(schema.contacts, schema.contacts.archived);
|
||||||
from2To3: (m, schema) async {
|
m.addColumn(
|
||||||
m.addColumn(schema.contacts, schema.contacts.archived);
|
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
|
||||||
m.addColumn(
|
}, from3To4: (m, schema) async {
|
||||||
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
|
m.createTable(mediaUploads);
|
||||||
},
|
}, from4To5: (m, schema) async {
|
||||||
from3To4: (m, schema) async {
|
m.createTable(mediaDownloads);
|
||||||
m.createTable(mediaUploads);
|
m.addColumn(schema.messages, schema.messages.mediaDownloadId);
|
||||||
},
|
m.addColumn(schema.messages, schema.messages.mediaUploadId);
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -798,10 +798,239 @@ i1.GeneratedColumn<String> _column_47(String aliasedName) =>
|
||||||
i1.GeneratedColumn<String>('already_notified', aliasedName, false,
|
i1.GeneratedColumn<String>('already_notified', aliasedName, false,
|
||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
defaultValue: const CustomExpression('\'[]\''));
|
defaultValue: const CustomExpression('\'[]\''));
|
||||||
|
|
||||||
|
final class Schema5 extends i0.VersionedSchema {
|
||||||
|
Schema5({required super.database}) : super(version: 5);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
contacts,
|
||||||
|
messages,
|
||||||
|
mediaUploads,
|
||||||
|
mediaDownloads,
|
||||||
|
signalIdentityKeyStores,
|
||||||
|
signalPreKeyStores,
|
||||||
|
signalSenderKeyStores,
|
||||||
|
signalSessionStores,
|
||||||
|
];
|
||||||
|
late final Shape6 contacts = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'contacts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(user_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 messages = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_22,
|
||||||
|
_column_23,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 mediaUploads = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'media_uploads',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape9 mediaDownloads = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'media_downloads',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape2 signalIdentityKeyStores = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_identity_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(device_id, name)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
_column_10,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape3 signalPreKeyStores = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(pre_key_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
_column_10,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape4 signalSenderKeyStores = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_sender_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(sender_key_name)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 signalSessionStores = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_session_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(device_id, name)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_38,
|
||||||
|
_column_10,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape8 extends i0.VersionedTable {
|
||||||
|
Shape8({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get contactId =>
|
||||||
|
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get messageId =>
|
||||||
|
columnsByName['message_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get messageOtherId =>
|
||||||
|
columnsByName['message_other_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get mediaUploadId =>
|
||||||
|
columnsByName['media_upload_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get mediaDownloadId =>
|
||||||
|
columnsByName['media_download_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get responseToMessageId =>
|
||||||
|
columnsByName['response_to_message_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get responseToOtherMessageId =>
|
||||||
|
columnsByName['response_to_other_message_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get acknowledgeByUser =>
|
||||||
|
columnsByName['acknowledge_by_user']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get downloadState =>
|
||||||
|
columnsByName['download_state']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get acknowledgeByServer =>
|
||||||
|
columnsByName['acknowledge_by_server']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get errorWhileSending =>
|
||||||
|
columnsByName['error_while_sending']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get kind =>
|
||||||
|
columnsByName['kind']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get contentJson =>
|
||||||
|
columnsByName['content_json']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get openedAt =>
|
||||||
|
columnsByName['opened_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get sendAt =>
|
||||||
|
columnsByName['send_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_48(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('media_upload_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_49(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('media_download_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
|
||||||
|
class Shape9 extends i0.VersionedTable {
|
||||||
|
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get messageId =>
|
||||||
|
columnsByName['message_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get downloadToken =>
|
||||||
|
columnsByName['download_token']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_50(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('message_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_51(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('download_token', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -820,6 +1049,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from3To4(migrator, schema);
|
await from3To4(migrator, schema);
|
||||||
return 4;
|
return 4;
|
||||||
|
case 4:
|
||||||
|
final schema = Schema5(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from4To5(migrator, schema);
|
||||||
|
return 5;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -830,10 +1064,12 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
from2To3: from2To3,
|
from2To3: from2To3,
|
||||||
from3To4: from3To4,
|
from3To4: from3To4,
|
||||||
|
from4To5: from4To5,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Profile'**
|
/// **'Profile'**
|
||||||
String get settingsProfile;
|
String get settingsProfile;
|
||||||
|
|
||||||
|
/// No description provided for @settingsStorageData.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Data and storage'**
|
||||||
|
String get settingsStorageData;
|
||||||
|
|
||||||
/// No description provided for @settingsProfileCustomizeAvatar.
|
/// No description provided for @settingsProfileCustomizeAvatar.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsProfile => 'Profil';
|
String get settingsProfile => 'Profil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageData => 'Daten und Speicher';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsProfile => 'Profile';
|
String get settingsProfile => 'Profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsStorageData => 'Data and storage';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ class MediaMessageContent extends MessageContent {
|
||||||
'encryptionNonce': encryptionNonce,
|
'encryptionNonce': encryptionNonce,
|
||||||
'isRealTwonly': isRealTwonly,
|
'isRealTwonly': isRealTwonly,
|
||||||
'maxShowTime': maxShowTime,
|
'maxShowTime': maxShowTime,
|
||||||
|
'isVideo': isVideo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/providers/api/media_send.dart';
|
import 'package:twonly/src/providers/api/media_send.dart';
|
||||||
import 'package:twonly/src/providers/hive.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
|
|
@ -17,7 +16,11 @@ import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart'
|
||||||
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/server_to_client.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/server_to_client.pbserver.dart';
|
||||||
|
|
||||||
|
Map<int, DateTime> downloadStartedForMediaReceived = {};
|
||||||
|
|
||||||
Future tryDownloadAllMediaFiles() async {
|
Future tryDownloadAllMediaFiles() async {
|
||||||
|
// this is called when websocket is newly connected, so allow all downloads to be restarted.
|
||||||
|
downloadStartedForMediaReceived = {};
|
||||||
List<Message> messages =
|
List<Message> messages =
|
||||||
await twonlyDatabase.messagesDao.getAllMessagesPendingDownloading();
|
await twonlyDatabase.messagesDao.getAllMessagesPendingDownloading();
|
||||||
|
|
||||||
|
|
@ -28,6 +31,14 @@ Future tryDownloadAllMediaFiles() async {
|
||||||
|
|
||||||
Future startDownloadMedia(Message message, bool force) async {
|
Future startDownloadMedia(Message message, bool force) async {
|
||||||
if (message.contentJson == null) return;
|
if (message.contentJson == null) return;
|
||||||
|
if (downloadStartedForMediaReceived[message.messageId] != null) {
|
||||||
|
DateTime started = downloadStartedForMediaReceived[message.messageId]!;
|
||||||
|
Duration elapsed = DateTime.now().difference(started);
|
||||||
|
if (elapsed <= Duration(seconds: 60)) {
|
||||||
|
Logger("media_received.dart").shout("Download already started...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final content =
|
final content =
|
||||||
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
||||||
|
|
@ -69,6 +80,8 @@ Future startDownloadMedia(Message message, bool force) async {
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
offset = bytes.length;
|
offset = bytes.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadStartedForMediaReceived[message.messageId] = DateTime.now();
|
||||||
apiProvider.triggerDownload(content.downloadToken!, offset);
|
apiProvider.triggerDownload(content.downloadToken!, offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,10 +95,6 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
||||||
Logger("server_messages")
|
Logger("server_messages")
|
||||||
.info("downloading: ${data.downloadToken} ${data.fin}");
|
.info("downloading: ${data.downloadToken} ${data.fin}");
|
||||||
|
|
||||||
final box = await getMediaStorage();
|
|
||||||
|
|
||||||
String boxId = data.downloadToken.toString();
|
|
||||||
|
|
||||||
final media = await twonlyDatabase.mediaDownloadsDao
|
final media = await twonlyDatabase.mediaDownloadsDao
|
||||||
.getMediaDownloadByDownloadToken(data.downloadToken)
|
.getMediaDownloadByDownloadToken(data.downloadToken)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
@ -128,9 +137,10 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
||||||
downloadedBytes = Uint8List.fromList(data.data);
|
downloadedBytes = Uint8List.fromList(data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await writeMediaFile(media.messageId, "encrypted", downloadedBytes);
|
||||||
|
|
||||||
if (!data.fin) {
|
if (!data.fin) {
|
||||||
// download not finished, so waiting for more data...
|
// download not finished, so waiting for more data...
|
||||||
await box.put(boxId, downloadedBytes);
|
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +150,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
|
await deleteMediaFile(media.messageId, "encrypted");
|
||||||
Logger("media_received.dart")
|
Logger("media_received.dart")
|
||||||
.info("messageId not found in database. Ignoring download");
|
.info("messageId not found in database. Ignoring download");
|
||||||
// answers with ok, so the server will delete the message
|
// answers with ok, so the server will delete the message
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
|
|
@ -18,7 +17,6 @@ import 'package:twonly/src/model/protobuf/api/server_to_client.pb.dart';
|
||||||
import 'package:twonly/src/providers/api/api.dart';
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
|
||||||
|
|
||||||
Future sendMediaFile(
|
Future sendMediaFile(
|
||||||
List<int> userIds,
|
List<int> userIds,
|
||||||
|
|
@ -34,6 +32,7 @@ Future sendMediaFile(
|
||||||
metadata.messageSendAt = DateTime.now();
|
metadata.messageSendAt = DateTime.now();
|
||||||
metadata.isVideo = videoFilePath != null;
|
metadata.isVideo = videoFilePath != null;
|
||||||
metadata.videoWithAudio = enableVideoAudio != null && enableVideoAudio;
|
metadata.videoWithAudio = enableVideoAudio != null && enableVideoAudio;
|
||||||
|
metadata.maxShowTime = maxShowTime;
|
||||||
|
|
||||||
int? mediaUploadId = await twonlyDatabase.mediaUploadsDao.insertMediaUpload(
|
int? mediaUploadId = await twonlyDatabase.mediaUploadsDao.insertMediaUpload(
|
||||||
MediaUploadsCompanion(
|
MediaUploadsCompanion(
|
||||||
|
|
@ -42,6 +41,11 @@ Future sendMediaFile(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mediaUploadId != null) {
|
if (mediaUploadId != null) {
|
||||||
|
if (videoFilePath != null) {
|
||||||
|
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||||
|
await File(videoFilePath.path).rename("$basePath.video");
|
||||||
|
}
|
||||||
|
await writeMediaFile(mediaUploadId, "image", imageBytes);
|
||||||
await handleSingleMediaFile(mediaUploadId);
|
await handleSingleMediaFile(mediaUploadId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,62 +61,62 @@ Future retryMediaUpload() async {
|
||||||
final lockingHandleMediaFile = Mutex();
|
final lockingHandleMediaFile = Mutex();
|
||||||
|
|
||||||
Future handleSingleMediaFile(int mediaUploadId) async {
|
Future handleSingleMediaFile(int mediaUploadId) async {
|
||||||
await lockingHandleMediaFile.protect(() async {
|
// await lockingHandleMediaFile.protect(() async {
|
||||||
MediaUpload? media = await twonlyDatabase.mediaUploadsDao
|
MediaUpload? media = await twonlyDatabase.mediaUploadsDao
|
||||||
.getMediaUploadById(mediaUploadId)
|
.getMediaUploadById(mediaUploadId)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (media == null) return;
|
if (media == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (media.state) {
|
switch (media.state) {
|
||||||
case UploadState.pending:
|
case UploadState.pending:
|
||||||
await handleAddToMessageDb(media);
|
await handleAddToMessageDb(media);
|
||||||
break;
|
break;
|
||||||
case UploadState.addedToMessagesDb:
|
case UploadState.addedToMessagesDb:
|
||||||
await handleCompressionState(media);
|
await handleCompressionState(media);
|
||||||
break;
|
break;
|
||||||
case UploadState.isCompressed:
|
case UploadState.isCompressed:
|
||||||
await handleEncryptionState(media);
|
await handleEncryptionState(media);
|
||||||
break;
|
break;
|
||||||
case UploadState.isEncrypted:
|
case UploadState.isEncrypted:
|
||||||
if (!await handleGetUploadToken(media)) {
|
if (!await handleGetUploadToken(media)) {
|
||||||
return; // recoverable error. try again when connected again to the server...
|
return; // recoverable error. try again when connected again to the server...
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UploadState.hasUploadToken:
|
|
||||||
if (!await handleUpload(media)) {
|
|
||||||
return; // recoverable error. try again when connected again to the server...
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UploadState.isUploaded:
|
|
||||||
if (!await handleNotifyReceiver(media)) {
|
|
||||||
return; // recoverable error. try again when connected again to the server...
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UploadState.receiverNotified:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will be called until there is an recoverable error OR
|
|
||||||
// the upload is ready
|
|
||||||
await handleSingleMediaFile(mediaUploadId);
|
|
||||||
} catch (e) {
|
|
||||||
// if the messageIds are already there notify the user about this error...
|
|
||||||
if (media.messageIds != null) {
|
|
||||||
for (int messageId in media.messageIds!) {
|
|
||||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
|
||||||
messageId,
|
|
||||||
MessagesCompanion(
|
|
||||||
errorWhileSending: Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
await twonlyDatabase.mediaUploadsDao.deleteMediaUpload(mediaUploadId);
|
case UploadState.hasUploadToken:
|
||||||
Logger("media_send.dart")
|
if (!await handleUpload(media)) {
|
||||||
.shout("Non recoverable error while sending media file: $e");
|
return; // recoverable error. try again when connected again to the server...
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UploadState.isUploaded:
|
||||||
|
if (!await handleNotifyReceiver(media)) {
|
||||||
|
return; // recoverable error. try again when connected again to the server...
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UploadState.receiverNotified:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// this will be called until there is an recoverable error OR
|
||||||
|
// the upload is ready
|
||||||
|
await handleSingleMediaFile(mediaUploadId);
|
||||||
|
} catch (e) {
|
||||||
|
// if the messageIds are already there notify the user about this error...
|
||||||
|
if (media.messageIds != null) {
|
||||||
|
for (int messageId in media.messageIds!) {
|
||||||
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(
|
||||||
|
errorWhileSending: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await twonlyDatabase.mediaUploadsDao.deleteMediaUpload(mediaUploadId);
|
||||||
|
Logger("media_send.dart")
|
||||||
|
.shout("Non recoverable error while sending media file: $e");
|
||||||
|
}
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
Future handleAddToMessageDb(MediaUpload media) async {
|
Future handleAddToMessageDb(MediaUpload media) async {
|
||||||
|
|
@ -177,11 +181,12 @@ Future handleCompressionState(MediaUpload media) async {
|
||||||
quality: 60,
|
quality: 60,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await writeMediaFile(media, "image.compressed", imageBytesCompressed);
|
await writeMediaFile(
|
||||||
|
media.mediaUploadId, "image.compressed", imageBytesCompressed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger("media_send.dart").shout("$e");
|
Logger("media_send.dart").shout("$e");
|
||||||
// as a fall back use the original image
|
// as a fall back use the original image
|
||||||
await writeMediaFile(media, "image.compressed", imageBytes);
|
await writeMediaFile(media.mediaUploadId, "image.compressed", imageBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.metadata.isVideo) {
|
if (media.metadata.isVideo) {
|
||||||
|
|
@ -189,36 +194,37 @@ Future handleCompressionState(MediaUpload media) async {
|
||||||
File videoOriginalFile = File("$basePath.video");
|
File videoOriginalFile = File("$basePath.video");
|
||||||
File videoCompressedFile = File("$basePath.video.compressed");
|
File videoCompressedFile = File("$basePath.video.compressed");
|
||||||
|
|
||||||
MediaInfo? mediaInfo;
|
// MediaInfo? mediaInfo;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaInfo = await VideoCompress.compressVideo(
|
// mediaInfo = await VideoCompress.compressVideo(
|
||||||
videoOriginalFile.path,
|
// videoOriginalFile.path,
|
||||||
quality: VideoQuality.Res1280x720Quality,
|
// quality: VideoQuality.Res1280x720Quality,
|
||||||
deleteOrigin: false,
|
// deleteOrigin: false,
|
||||||
includeAudio: media.metadata.videoWithAudio,
|
// includeAudio: media.metadata.videoWithAudio,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (mediaInfo!.filesize! >= 20 * 1000 * 1000) {
|
// if (mediaInfo!.filesize! >= 20 * 1000 * 1000) {
|
||||||
// if the media file is over 20MB compress it with low quality
|
// // if the media file is over 20MB compress it with low quality
|
||||||
mediaInfo = await VideoCompress.compressVideo(
|
// mediaInfo = await VideoCompress.compressVideo(
|
||||||
videoOriginalFile.path,
|
// videoOriginalFile.path,
|
||||||
quality: VideoQuality.Res960x540Quality,
|
// quality: VideoQuality.Res960x540Quality,
|
||||||
deleteOrigin: false,
|
// deleteOrigin: false,
|
||||||
includeAudio: media.metadata.videoWithAudio,
|
// includeAudio: media.metadata.videoWithAudio,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger("media_send.dart").shout("$e");
|
Logger("media_send.dart").shout("Video compression: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaInfo == null) {
|
// if (mediaInfo == null) {
|
||||||
Logger("media_send.dart").shout("Error compressing video.");
|
// as a fall back use the non compressed version
|
||||||
mediaInfo!.file!.rename(videoCompressedFile.path);
|
await videoOriginalFile.copy(videoCompressedFile.path);
|
||||||
} else {
|
await videoOriginalFile.delete();
|
||||||
// as a fall back use the non compressed version
|
// } else {
|
||||||
videoOriginalFile.rename(videoCompressedFile.path);
|
// Logger("media_send.dart").shout("Error compressing video.");
|
||||||
}
|
// mediaInfo!.file!.rename(videoCompressedFile.path);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete non compressed media files
|
// delete non compressed media files
|
||||||
|
|
@ -263,7 +269,7 @@ Future handleEncryptionState(MediaUpload media) async {
|
||||||
state.sha2Hash = (await algorithm.hash(secretBox.cipherText)).bytes;
|
state.sha2Hash = (await algorithm.hash(secretBox.cipherText)).bytes;
|
||||||
|
|
||||||
await writeMediaFile(
|
await writeMediaFile(
|
||||||
media,
|
media.mediaUploadId,
|
||||||
"encrypted",
|
"encrypted",
|
||||||
Uint8List.fromList(secretBox.cipherText),
|
Uint8List.fromList(secretBox.cipherText),
|
||||||
);
|
);
|
||||||
|
|
@ -427,8 +433,8 @@ Future<Uint8List> readMediaFile(MediaUpload media, String type) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeMediaFile(
|
Future<void> writeMediaFile(
|
||||||
MediaUpload media, String type, Uint8List data) async {
|
int mediaUploadId, String type, Uint8List data) async {
|
||||||
String basePath = await getMediaFilePath(media.mediaUploadId, "send");
|
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||||
File file = File("$basePath.$type");
|
File file = File("$basePath.$type");
|
||||||
await file.writeAsBytes(data);
|
await file.writeAsBytes(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!context.mounted) return true;
|
if (!context.mounted) return true;
|
||||||
if (shoudReturn != null && shoudReturn) {
|
if (shoudReturn == null) return true;
|
||||||
|
if (shoudReturn) {
|
||||||
if (!context.mounted) return true;
|
if (!context.mounted) return true;
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|
@ -438,7 +439,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
// Positioned.fill(
|
// Positioned.fill(
|
||||||
// child: GestureDetector(),
|
// child: GestureDetector(),
|
||||||
// ),
|
// ),
|
||||||
if (!sharePreviewIsShown && widget.sendTo != null)
|
if (!sharePreviewIsShown &&
|
||||||
|
widget.sendTo != null &&
|
||||||
|
!isVideoRecording)
|
||||||
SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)),
|
SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)),
|
||||||
if (!sharePreviewIsShown && !isVideoRecording)
|
if (!sharePreviewIsShown && !isVideoRecording)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
@ -15,7 +15,6 @@ import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_view.dart';
|
import 'package:twonly/src/views/camera/share_image_view.dart';
|
||||||
import 'dart:async';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
|
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
|
||||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||||
|
|
@ -44,7 +43,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
int maxShowTime = 999999;
|
int maxShowTime = 999999;
|
||||||
String? sendNextMediaToUserName;
|
String? sendNextMediaToUserName;
|
||||||
double tabDownPostion = 0;
|
double tabDownPostion = 0;
|
||||||
bool sendingImage = false;
|
bool sendingOrLoadingImage = true;
|
||||||
|
bool isDisposed = false;
|
||||||
double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
|
double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
|
||||||
VideoPlayerController? videoController;
|
VideoPlayerController? videoController;
|
||||||
|
|
||||||
|
|
@ -58,6 +58,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
if (widget.imageBytes != null) {
|
if (widget.imageBytes != null) {
|
||||||
loadImage(widget.imageBytes!);
|
loadImage(widget.imageBytes!);
|
||||||
} else if (widget.videoFilePath != null) {
|
} else if (widget.videoFilePath != null) {
|
||||||
|
setState(() {
|
||||||
|
sendingOrLoadingImage = false;
|
||||||
|
});
|
||||||
videoController =
|
videoController =
|
||||||
VideoPlayerController.file(File(widget.videoFilePath!.path));
|
VideoPlayerController.file(File(widget.videoFilePath!.path));
|
||||||
videoController?.setLooping(true);
|
videoController?.setLooping(true);
|
||||||
|
|
@ -67,8 +70,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
}).catchError((Object error) {
|
}).catchError((Object error) {
|
||||||
Logger("ui.share_image_editor").shout(error);
|
Logger("ui.share_image_editor").shout(error);
|
||||||
});
|
});
|
||||||
videoController?.play();
|
|
||||||
print(widget.videoFilePath!.path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
isDisposed = true;
|
||||||
layers.clear();
|
layers.clear();
|
||||||
videoController?.dispose();
|
videoController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -152,11 +154,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
NotificationBadge(
|
NotificationBadge(
|
||||||
count: maxShowTime == 999999 ? "∞" : maxShowTime.toString(),
|
count: (widget.videoFilePath != null)
|
||||||
|
? "0"
|
||||||
|
: maxShowTime == 999999
|
||||||
|
? "∞"
|
||||||
|
: maxShowTime.toString(),
|
||||||
child: ActionButton(
|
child: ActionButton(
|
||||||
Icons.timer_outlined,
|
(widget.videoFilePath != null)
|
||||||
|
? maxShowTime == 1
|
||||||
|
? Icons.repeat_rounded
|
||||||
|
: Icons.repeat_one_rounded
|
||||||
|
: Icons.timer_outlined,
|
||||||
tooltipText: context.lang.protectAsARealTwonly,
|
tooltipText: context.lang.protectAsARealTwonly,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (widget.videoFilePath != null) {
|
||||||
|
setState(() {
|
||||||
|
maxShowTime = (maxShowTime + 1) % 2;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (maxShowTime == 999999) {
|
if (maxShowTime == 999999) {
|
||||||
maxShowTime = 4;
|
maxShowTime = 4;
|
||||||
} else if (maxShowTime >= 22) {
|
} else if (maxShowTime >= 22) {
|
||||||
|
|
@ -276,7 +292,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
Future<Uint8List?> getMergedImage() async {
|
Future<Uint8List?> getMergedImage() async {
|
||||||
Uint8List? image;
|
Uint8List? image;
|
||||||
|
|
||||||
if (layers.length > 1) {
|
if (layers.length > 1 || widget.videoFilePath != null) {
|
||||||
for (var x in layers) {
|
for (var x in layers) {
|
||||||
x.showCustomButtons = false;
|
x.showCustomButtons = false;
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +313,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
Future<void> loadImage(Future<Uint8List?> imageFile) async {
|
Future<void> loadImage(Future<Uint8List?> imageFile) async {
|
||||||
Uint8List? imageBytes = await imageFile;
|
Uint8List? imageBytes = await imageFile;
|
||||||
await currentImage.load(imageBytes);
|
await currentImage.load(imageBytes);
|
||||||
|
if (isDisposed) return;
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
|
@ -307,21 +324,31 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
));
|
));
|
||||||
|
|
||||||
layers.add(FilterLayerData());
|
layers.add(FilterLayerData());
|
||||||
|
setState(() {
|
||||||
|
sendingOrLoadingImage = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future sendImageToSinglePerson() async {
|
Future sendImageToSinglePerson() async {
|
||||||
|
if (sendingOrLoadingImage) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingImage = true;
|
sendingOrLoadingImage = true;
|
||||||
});
|
});
|
||||||
Uint8List? imageBytes = await getMergedImage();
|
Uint8List? imageBytes = await getMergedImage();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (imageBytes == null) {
|
if (imageBytes == null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendMediaFile([widget.sendTo!.userId], imageBytes, _isRealTwonly,
|
sendMediaFile(
|
||||||
maxShowTime, widget.videoFilePath, videoWithAudio);
|
[widget.sendTo!.userId],
|
||||||
|
imageBytes,
|
||||||
|
_isRealTwonly,
|
||||||
|
maxShowTime,
|
||||||
|
widget.videoFilePath,
|
||||||
|
videoWithAudio,
|
||||||
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
|
@ -434,7 +461,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
SizedBox(width: sendNextMediaToUserName == null ? 20 : 10),
|
SizedBox(width: sendNextMediaToUserName == null ? 20 : 10),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: sendingImage
|
icon: sendingOrLoadingImage
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
width: 12,
|
width: 12,
|
||||||
|
|
@ -445,7 +472,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
)
|
)
|
||||||
: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (sendingImage) return;
|
if (sendingOrLoadingImage) return;
|
||||||
if (widget.sendTo == null) return pushShareImageView();
|
if (widget.sendTo == null) return pushShareImageView();
|
||||||
sendImageToSinglePerson();
|
sendImageToSinglePerson();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ class ChatListEntry extends StatelessWidget {
|
||||||
child: BetterText(text: content.text),
|
child: BetterText(text: content.text),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (content is MediaMessageContent && !content.isVideo) {
|
} else if (content is MediaMessageContent) {
|
||||||
Color color = getMessageColorFromType(
|
Color color = getMessageColorFromType(
|
||||||
content,
|
content,
|
||||||
Theme.of(context).colorScheme.primary,
|
Theme.of(context).colorScheme.primary,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:drift/drift.dart' hide Column;
|
import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:no_screenshot/no_screenshot.dart';
|
import 'package:no_screenshot/no_screenshot.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -18,6 +20,7 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
final _noScreenshot = NoScreenshot.instance;
|
final _noScreenshot = NoScreenshot.instance;
|
||||||
|
|
||||||
|
|
@ -37,6 +40,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
|
|
||||||
// current image related
|
// current image related
|
||||||
Uint8List? imageBytes;
|
Uint8List? imageBytes;
|
||||||
|
VideoPlayerController? videoController;
|
||||||
|
|
||||||
DateTime? canBeSeenUntil;
|
DateTime? canBeSeenUntil;
|
||||||
int maxShowTime = 999999;
|
int maxShowTime = 999999;
|
||||||
double progress = 0;
|
double progress = 0;
|
||||||
|
|
@ -47,6 +52,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
bool imageSaved = false;
|
bool imageSaved = false;
|
||||||
bool imageSaving = false;
|
bool imageSaving = false;
|
||||||
|
|
||||||
|
StreamSubscription<Message?>? downloadStateListener;
|
||||||
|
|
||||||
List<Message> allMediaFiles = [];
|
List<Message> allMediaFiles = [];
|
||||||
late StreamSubscription<List<Message>> _subscription;
|
late StreamSubscription<List<Message>> _subscription;
|
||||||
TextEditingController textMessageController = TextEditingController();
|
TextEditingController textMessageController = TextEditingController();
|
||||||
|
|
@ -97,7 +104,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allMediaFiles.removeAt(0);
|
allMediaFiles.removeAt(0);
|
||||||
loadCurrentMediaFile();
|
await loadCurrentMediaFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,11 +112,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
await _noScreenshot.screenshotOff();
|
await _noScreenshot.screenshotOff();
|
||||||
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
|
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
|
||||||
|
|
||||||
final current = allMediaFiles.first;
|
|
||||||
final MediaMessageContent content =
|
|
||||||
MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
videoController = null;
|
||||||
imageBytes = null;
|
imageBytes = null;
|
||||||
canBeSeenUntil = null;
|
canBeSeenUntil = null;
|
||||||
maxShowTime = 999999;
|
maxShowTime = 999999;
|
||||||
|
|
@ -121,78 +125,107 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
showSendTextMessageInput = false;
|
showSendTextMessageInput = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
flutterLocalNotificationsPlugin.cancel(allMediaFiles.first.contactId);
|
||||||
|
if (allMediaFiles.first.downloadState != DownloadState.downloaded) {
|
||||||
|
setState(() {
|
||||||
|
isDownloading = true;
|
||||||
|
});
|
||||||
|
await startDownloadMedia(allMediaFiles.first, true);
|
||||||
|
|
||||||
|
final stream = twonlyDatabase.messagesDao
|
||||||
|
.getMessageByMessageId(allMediaFiles.first.messageId)
|
||||||
|
.watchSingleOrNull();
|
||||||
|
downloadStateListener?.cancel();
|
||||||
|
downloadStateListener = stream.listen((updated) async {
|
||||||
|
if (updated != null) {
|
||||||
|
if (updated.downloadState == DownloadState.downloaded) {
|
||||||
|
downloadStateListener?.cancel();
|
||||||
|
await handleNextDownloadedMedia(updated, showTwonly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await handleNextDownloadedMedia(allMediaFiles.first, showTwonly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future handleNextDownloadedMedia(Message current, bool showTwonly) async {
|
||||||
|
final MediaMessageContent content =
|
||||||
|
MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
|
||||||
|
|
||||||
if (content.isRealTwonly) {
|
if (content.isRealTwonly) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isRealTwonly = true;
|
isRealTwonly = true;
|
||||||
});
|
});
|
||||||
if (!showTwonly) {
|
if (!showTwonly) return;
|
||||||
|
|
||||||
|
bool isAuth = await authenticateUser(
|
||||||
|
context.lang.mediaViewerAuthReason,
|
||||||
|
force: false,
|
||||||
|
);
|
||||||
|
if (!isAuth) {
|
||||||
|
nextMediaOrExit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isRealTwonly) {
|
notifyContactAboutOpeningMessage(
|
||||||
if (!context.mounted) return;
|
current.contactId,
|
||||||
// ignore: use_build_context_synchronously
|
[current.messageOtherId!],
|
||||||
bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason,
|
);
|
||||||
force: false);
|
|
||||||
if (!isAuth) {
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
nextMediaOrExit();
|
current.messageId,
|
||||||
return;
|
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (content.isVideo) {
|
||||||
|
final vidoePath = await getVideoPath(current.messageId);
|
||||||
|
if (vidoePath != null) {
|
||||||
|
videoController = VideoPlayerController.file(File(vidoePath.path));
|
||||||
|
videoController?.setLooping(content.maxShowTime == 1);
|
||||||
|
if (content.maxShowTime == 0) {
|
||||||
|
videoController?.addListener(() {
|
||||||
|
if (videoController?.value.position ==
|
||||||
|
videoController?.value.duration) {
|
||||||
|
nextMediaOrExit();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
videoController?.initialize().then((_) {
|
||||||
|
videoController!.play();
|
||||||
|
setState(() {});
|
||||||
|
}).catchError((Object error) {
|
||||||
|
Logger("media_viewer_view.dart").shout(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flutterLocalNotificationsPlugin.cancel(current.contactId);
|
imageBytes = await getImageBytes(current.messageId);
|
||||||
if (current.downloadState == DownloadState.pending) {
|
|
||||||
setState(() {
|
|
||||||
isDownloading = true;
|
|
||||||
});
|
|
||||||
await startDownloadMedia(current, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if ((imageBytes == null && !content.isVideo) ||
|
||||||
load downloaded status from database
|
(content.isVideo && videoController == null)) {
|
||||||
|
// When the message should be downloaded but imageBytes are null then a error happened
|
||||||
notifyContactAboutOpeningMessage(
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
message.contactId, [message.messageOtherId!]);
|
current.messageId,
|
||||||
twonlyDatabase.messagesDao.updateMessageByMessageId(
|
MessagesCompanion(
|
||||||
message.messageId, MessagesCompanion(openedAt: Value(DateTime.now())));
|
errorWhileSending: Value(true),
|
||||||
// do {
|
),
|
||||||
// if (isDownloading) {
|
|
||||||
// await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
// if (!apiProvider.isConnected) break;
|
|
||||||
// }
|
|
||||||
// if (content.downloadToken == null) break;
|
|
||||||
// imageBytes = await getDownloadedMedia(current, content.downloadToken!);
|
|
||||||
// } while (isDownloading && imageBytes == null);
|
|
||||||
|
|
||||||
if twonly deleteMediaFile()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
isDownloading = false;
|
|
||||||
if (imageBytes == null) {
|
|
||||||
if (current.downloadState == DownloadState.downloaded) {
|
|
||||||
// When the message should be downloaded but imageBytes are null then a error happened
|
|
||||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
|
||||||
current.messageId,
|
|
||||||
MessagesCompanion(
|
|
||||||
errorWhileSending: Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextMediaOrExit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.maxShowTime != 999999) {
|
|
||||||
canBeSeenUntil = DateTime.now().add(
|
|
||||||
Duration(seconds: content.maxShowTime),
|
|
||||||
);
|
);
|
||||||
maxShowTime = content.maxShowTime;
|
return nextMediaOrExit();
|
||||||
startTimer();
|
|
||||||
}
|
}
|
||||||
setState(() {});
|
|
||||||
|
if (!content.isVideo) {
|
||||||
|
if (content.maxShowTime != 999999) {
|
||||||
|
canBeSeenUntil = DateTime.now().add(
|
||||||
|
Duration(seconds: content.maxShowTime),
|
||||||
|
);
|
||||||
|
maxShowTime = content.maxShowTime;
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isDownloading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startTimer() {
|
startTimer() {
|
||||||
|
|
@ -215,11 +248,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
nextMediaTimer?.cancel();
|
nextMediaTimer?.cancel();
|
||||||
progressTimer?.cancel();
|
progressTimer?.cancel();
|
||||||
_noScreenshot.screenshotOn();
|
_noScreenshot.screenshotOn();
|
||||||
_subscription.cancel();
|
_subscription.cancel();
|
||||||
|
downloadStateListener?.cancel();
|
||||||
|
videoController?.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future onPressedSaveToGallery() async {
|
Future onPressedSaveToGallery() async {
|
||||||
|
|
@ -357,7 +392,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (imageBytes != null && (canBeSeenUntil == null || progress >= 0))
|
if ((imageBytes != null || videoController != null) &&
|
||||||
|
(canBeSeenUntil == null || progress >= 0))
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (showSendTextMessageInput) {
|
if (showSendTextMessageInput) {
|
||||||
|
|
@ -372,24 +408,34 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
child: MediaViewSizing(
|
child: MediaViewSizing(
|
||||||
bottomNavigation: bottomNavigation(),
|
bottomNavigation: bottomNavigation(),
|
||||||
requiredHeight: 80,
|
requiredHeight: 80,
|
||||||
child: Image.memory(
|
child: Stack(
|
||||||
imageBytes!,
|
children: [
|
||||||
fit: BoxFit.contain,
|
if (videoController != null)
|
||||||
frameBuilder:
|
Positioned.fill(child: VideoPlayer(videoController!)),
|
||||||
((context, child, frame, wasSynchronouslyLoaded) {
|
if (imageBytes != null)
|
||||||
if (wasSynchronouslyLoaded) return child;
|
Positioned.fill(
|
||||||
return AnimatedSwitcher(
|
child: Image.memory(
|
||||||
duration: const Duration(milliseconds: 200),
|
imageBytes!,
|
||||||
child: frame != null
|
fit: BoxFit.contain,
|
||||||
? child
|
frameBuilder: ((context, child, frame,
|
||||||
: SizedBox(
|
wasSynchronouslyLoaded) {
|
||||||
height: 60,
|
if (wasSynchronouslyLoaded) return child;
|
||||||
width: 60,
|
return AnimatedSwitcher(
|
||||||
child:
|
duration: const Duration(milliseconds: 200),
|
||||||
CircularProgressIndicator(strokeWidth: 2),
|
child: frame != null
|
||||||
),
|
? child
|
||||||
);
|
: SizedBox(
|
||||||
}),
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -439,12 +485,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
if (canBeSeenUntil != null)
|
||||||
right: 20,
|
Positioned(
|
||||||
top: 27,
|
right: 20,
|
||||||
child: Row(
|
top: 27,
|
||||||
children: [
|
child: Row(
|
||||||
if (canBeSeenUntil != null)
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
|
|
@ -453,9 +499,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
strokeWidth: 2.0,
|
strokeWidth: 2.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (showSendTextMessageInput)
|
if (showSendTextMessageInput)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
if (message.errorWhileSending) {
|
if (message.errorWhileSending) {
|
||||||
icon =
|
icon =
|
||||||
FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color);
|
FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color);
|
||||||
text = "Unknown error.";
|
text = "Error";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.kind == MessageKind.media) {
|
if (message.kind == MessageKind.media) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
import 'schema_v4.dart' as v4;
|
import 'schema_v4.dart' as v4;
|
||||||
|
import 'schema_v5.dart' as v5;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -20,10 +21,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v3.DatabaseAtV3(db);
|
return v3.DatabaseAtV3(db);
|
||||||
case 4:
|
case 4:
|
||||||
return v4.DatabaseAtV4(db);
|
return v4.DatabaseAtV4(db);
|
||||||
|
case 5:
|
||||||
|
return v5.DatabaseAtV5(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4];
|
static const versions = const [1, 2, 3, 4, 5];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2820
test/drift/twonly_database/generated/schema_v5.dart
Normal file
2820
test/drift/twonly_database/generated/schema_v5.dart
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue