diff --git a/lib/app.dart b/lib/app.dart index 96588e9..48cc497 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,7 +6,6 @@ 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/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'; @@ -68,8 +67,6 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); await apiService.connect(force: true); await apiService.listenToNetworkChanges(); - // call this function so invalid media files are get purged - await retryMediaUpload(true); } @override @@ -84,7 +81,6 @@ class _AppState extends State with WidgetsBindingObserver { } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; - unawaited(handleUploadWhenAppGoesBackground()); } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index a6832f7..72d5062 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -25,6 +25,14 @@ class MediaFilesDao extends DatabaseAccessor } } + Future deleteMediaFile(String mediaId) async { + await (delete(mediaFiles) + ..where( + (t) => t.mediaId.equals(mediaId), + )) + .go(); + } + Future updateMedia( String mediaId, MediaFilesCompanion updates, @@ -57,4 +65,8 @@ class MediaFilesDao extends DatabaseAccessor ..where((t) => t.downloadState.equals(DownloadState.pending.name))) .get(); } + + Stream> watchAllStoredMediaFiles() { + return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); + } } diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index faf0053..11651c8 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -4,6 +4,8 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +enum MessageType { media, text } + @DataClassName('Message') class Messages extends Table { TextColumn get groupId => @@ -14,9 +16,12 @@ class Messages extends Table { IntColumn get senderId => integer().nullable().references(Contacts, #userId)(); + TextColumn get type => textEnum()(); + TextColumn get content => text().nullable()(); - TextColumn get mediaId => - text().nullable().references(MediaFiles, #mediaId)(); + TextColumn get mediaId => text() + .nullable() + .references(MediaFiles, #mediaId, onDelete: KeyAction.cascade)(); BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 69e7a22..a285163 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2254,6 +2254,11 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($MessagesTable.$convertertype); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -2268,7 +2273,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES media_files (media_id)')); + 'REFERENCES media_files (media_id) ON DELETE CASCADE')); static const VerificationMeta _mediaStoredMeta = const VerificationMeta('mediaStored'); @override @@ -2327,6 +2332,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { groupId, messageId, senderId, + type, content, mediaId, mediaStored, @@ -2415,6 +2421,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, senderId: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + type: $MessagesTable.$convertertype.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), content: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping @@ -2438,12 +2446,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { $MessagesTable createAlias(String alias) { return $MessagesTable(attachedDatabase, alias); } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MessageType.values); } class Message extends DataClass implements Insertable { final String groupId; final String messageId; final int? senderId; + final MessageType type; final String? content; final String? mediaId; final bool mediaStored; @@ -2456,6 +2468,7 @@ class Message extends DataClass implements Insertable { {required this.groupId, required this.messageId, this.senderId, + required this.type, this.content, this.mediaId, required this.mediaStored, @@ -2472,6 +2485,9 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || senderId != null) { map['sender_id'] = Variable(senderId); } + { + map['type'] = Variable($MessagesTable.$convertertype.toSql(type)); + } if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -2498,6 +2514,7 @@ class Message extends DataClass implements Insertable { senderId: senderId == null && nullToAbsent ? const Value.absent() : Value(senderId), + type: Value(type), content: content == null && nullToAbsent ? const Value.absent() : Value(content), @@ -2524,6 +2541,8 @@ class Message extends DataClass implements Insertable { groupId: serializer.fromJson(json['groupId']), messageId: serializer.fromJson(json['messageId']), senderId: serializer.fromJson(json['senderId']), + type: $MessagesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), mediaStored: serializer.fromJson(json['mediaStored']), @@ -2542,6 +2561,8 @@ class Message extends DataClass implements Insertable { 'groupId': serializer.toJson(groupId), 'messageId': serializer.toJson(messageId), 'senderId': serializer.toJson(senderId), + 'type': + serializer.toJson($MessagesTable.$convertertype.toJson(type)), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), 'mediaStored': serializer.toJson(mediaStored), @@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable { {String? groupId, String? messageId, Value senderId = const Value.absent(), + MessageType? type, Value content = const Value.absent(), Value mediaId = const Value.absent(), bool? mediaStored, @@ -2569,6 +2591,7 @@ class Message extends DataClass implements Insertable { groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, mediaStored: mediaStored ?? this.mediaStored, @@ -2586,6 +2609,7 @@ class Message extends DataClass implements Insertable { groupId: data.groupId.present ? data.groupId.value : this.groupId, messageId: data.messageId.present ? data.messageId.value : this.messageId, senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, mediaStored: @@ -2610,6 +2634,7 @@ class Message extends DataClass implements Insertable { ..write('groupId: $groupId, ') ..write('messageId: $messageId, ') ..write('senderId: $senderId, ') + ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') ..write('mediaStored: $mediaStored, ') @@ -2627,6 +2652,7 @@ class Message extends DataClass implements Insertable { groupId, messageId, senderId, + type, content, mediaId, mediaStored, @@ -2642,6 +2668,7 @@ class Message extends DataClass implements Insertable { other.groupId == this.groupId && other.messageId == this.messageId && other.senderId == this.senderId && + other.type == this.type && other.content == this.content && other.mediaId == this.mediaId && other.mediaStored == this.mediaStored && @@ -2656,6 +2683,7 @@ class MessagesCompanion extends UpdateCompanion { final Value groupId; final Value messageId; final Value senderId; + final Value type; final Value content; final Value mediaId; final Value mediaStored; @@ -2669,6 +2697,7 @@ class MessagesCompanion extends UpdateCompanion { this.groupId = const Value.absent(), this.messageId = const Value.absent(), this.senderId = const Value.absent(), + this.type = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), this.mediaStored = const Value.absent(), @@ -2683,6 +2712,7 @@ class MessagesCompanion extends UpdateCompanion { required String groupId, this.messageId = const Value.absent(), this.senderId = const Value.absent(), + required MessageType type, this.content = const Value.absent(), this.mediaId = const Value.absent(), this.mediaStored = const Value.absent(), @@ -2692,11 +2722,13 @@ class MessagesCompanion extends UpdateCompanion { this.isEdited = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : groupId = Value(groupId); + }) : groupId = Value(groupId), + type = Value(type); static Insertable custom({ Expression? groupId, Expression? messageId, Expression? senderId, + Expression? type, Expression? content, Expression? mediaId, Expression? mediaStored, @@ -2711,6 +2743,7 @@ class MessagesCompanion extends UpdateCompanion { if (groupId != null) 'group_id': groupId, if (messageId != null) 'message_id': messageId, if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, if (mediaStored != null) 'media_stored': mediaStored, @@ -2728,6 +2761,7 @@ class MessagesCompanion extends UpdateCompanion { {Value? groupId, Value? messageId, Value? senderId, + Value? type, Value? content, Value? mediaId, Value? mediaStored, @@ -2741,6 +2775,7 @@ class MessagesCompanion extends UpdateCompanion { groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, senderId: senderId ?? this.senderId, + type: type ?? this.type, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, mediaStored: mediaStored ?? this.mediaStored, @@ -2765,6 +2800,10 @@ class MessagesCompanion extends UpdateCompanion { if (senderId.present) { map['sender_id'] = Variable(senderId.value); } + if (type.present) { + map['type'] = + Variable($MessagesTable.$convertertype.toSql(type.value)); + } if (content.present) { map['content'] = Variable(content.value); } @@ -2801,6 +2840,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('groupId: $groupId, ') ..write('messageId: $messageId, ') ..write('senderId: $senderId, ') + ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') ..write('mediaStored: $mediaStored, ') @@ -6149,6 +6189,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('messages', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('media_files', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('messages', limitUpdateKind: UpdateKind.delete), @@ -7817,6 +7864,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String groupId, Value messageId, Value senderId, + required MessageType type, Value content, Value mediaId, Value mediaStored, @@ -7831,6 +7879,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value groupId, Value messageId, Value senderId, + Value type, Value content, Value mediaId, Value mediaStored, @@ -7984,6 +8033,11 @@ class $$MessagesTableFilterComposer ColumnFilters get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); @@ -8180,6 +8234,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); @@ -8293,6 +8350,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get messageId => $composableBuilder(column: $table.messageId, builder: (column) => column); + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); @@ -8510,6 +8570,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value groupId = const Value.absent(), Value messageId = const Value.absent(), Value senderId = const Value.absent(), + Value type = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), Value mediaStored = const Value.absent(), @@ -8524,6 +8585,7 @@ class $$MessagesTableTableManager extends RootTableManager< groupId: groupId, messageId: messageId, senderId: senderId, + type: type, content: content, mediaId: mediaId, mediaStored: mediaStored, @@ -8538,6 +8600,7 @@ class $$MessagesTableTableManager extends RootTableManager< required String groupId, Value messageId = const Value.absent(), Value senderId = const Value.absent(), + required MessageType type, Value content = const Value.absent(), Value mediaId = const Value.absent(), Value mediaStored = const Value.absent(), @@ -8552,6 +8615,7 @@ class $$MessagesTableTableManager extends RootTableManager< groupId: groupId, messageId: messageId, senderId: senderId, + type: type, content: content, mediaId: mediaId, mediaStored: mediaStored, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index de2ce86..2f17535 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -1,88 +1,30 @@ -import 'dart:convert'; -import 'dart:io'; - -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/mediafiles/upload.service.dart' as send; -import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; class MemoryItem { MemoryItem({ - required this.id, + required this.mediaService, required this.messages, - required this.date, - required this.mirrorVideo, - required this.thumbnailPath, - this.imagePath, - this.videoPath, }); - final int id; - final bool mirrorVideo; final List messages; - final DateTime date; - final File thumbnailPath; - final File? imagePath; - final File? videoPath; + final MediaFileService mediaService; - static Future> convertFromMessages( + static Future> convertFromMessages( List messages, ) async { - final items = {}; + final items = {}; for (final message in messages) { - final isSend = message.messageOtherId == null; - final id = message.mediaUploadId ?? message.messageId; - final basePath = await send.getMediaFilePath( - id, - isSend ? 'send' : 'received', - ); - File? imagePath; - late File thumbnailFile; - File? videoPath; - if (File('$basePath.mp4').existsSync()) { - videoPath = File('$basePath.mp4'); - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else if (File('$basePath.png').existsSync()) { - imagePath = File('$basePath.png'); - thumbnailFile = getThumbnailPath(imagePath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForImage(imagePath); - } - } else { - if (message.mediaStored) { - /// media file was deleted, ... remove the file - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - mediaStored: Value(false), - ), - ); - } - continue; - } - var mirrorVideo = false; - if (videoPath != null) { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - mirrorVideo = content.mirrorVideo; - } + if (message.mediaId == null) continue; + + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService == null) continue; items .putIfAbsent( - id, + message.mediaId!, () => MemoryItem( - id: id, + mediaService: mediaService, messages: [], - date: message.sendAt, - mirrorVideo: mirrorVideo, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, ), ) .messages diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index ec33119..e217869 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -6,6 +6,7 @@ import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:pie_menu/pie_menu.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; @@ -258,3 +259,28 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList( (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), ), ); + +PieTheme getPieCanvasTheme(BuildContext context) { + return PieTheme( + brightness: Theme.of(context).brightness, + rightClickShowsMenu: true, + radius: 70, + buttonTheme: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.tertiary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + buttonThemeHovered: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + tooltipPadding: const EdgeInsets.all(20), + overlayColor: isDarkMode(context) + ? const Color.fromARGB(69, 0, 0, 0) + : const Color.fromARGB(40, 0, 0, 0), + // spacing: 0, + tooltipTextStyle: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index ff66dc6..ba13f90 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -71,8 +71,14 @@ class _ShareImageEditorView extends State { selectedGroupIds.add(widget.sendToGroup!.groupId); } - if (widget.imageBytesFuture != null) { - unawaited(loadImage(widget.imageBytesFuture!)); + if (widget.mediaFileService.mediaFile.type == MediaType.image) { + if (widget.imageBytesFuture != null) { + loadImage(widget.imageBytesFuture!); + } else { + if (widget.mediaFileService.storedPath.existsSync()) { + loadImage(widget.mediaFileService.storedPath.readAsBytes()); + } + } } if (media.type == MediaType.video) { diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index f197cd5..302ad66 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -8,7 +8,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -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'; @@ -28,7 +27,7 @@ import 'package:twonly/src/views/chats/start_new_chat.view.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 8d12d25..603c4cb 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -23,7 +23,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry. import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; @@ -61,9 +61,9 @@ class ChatItem { /// Displays detailed information about a SampleItem. class ChatMessagesView extends StatefulWidget { - const ChatMessagesView(this.contact, {super.key}); + const ChatMessagesView(this.group, {super.key}); - final Contact contact; + final Group group; @override State createState() => _ChatMessagesViewState(); diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index affe878..d4dbcac 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -12,7 +12,7 @@ import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; class StartNewChatView extends StatefulWidget { const StartNewChatView({super.key}); diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart new file mode 100644 index 0000000..67605f7 --- /dev/null +++ b/lib/src/views/components/group_context_menu.component.dart @@ -0,0 +1,96 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; + +class GroupContextMenu extends StatefulWidget { + const GroupContextMenu({ + required this.group, + required this.child, + super.key, + }); + final Widget child; + final Group group; + + @override + State createState() => _GroupContextMenuState(); +} + +class _GroupContextMenuState extends State { + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => (), + onToggle: (menuOpen) async { + if (menuOpen) { + await HapticFeedback.heavyImpact(); + } + }, + actions: [ + if (!widget.group.archived) + PieAction( + tooltip: Text(context.lang.contextMenuArchiveUser), + onSelect: () async { + const update = GroupsCompanion(archived: Value(true)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: const FaIcon(FontAwesomeIcons.boxArchive), + ), + if (widget.group.archived) + PieAction( + tooltip: Text(context.lang.contextMenuUndoArchiveUser), + onSelect: () async { + const update = GroupsCompanion(archived: Value(false)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: const FaIcon(FontAwesomeIcons.boxOpen), + ), + PieAction( + tooltip: Text(context.lang.contextMenuOpenChat), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(widget.group); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.solidComments), + ), + PieAction( + tooltip: Text( + widget.group.pinned + ? context.lang.contextMenuUnpin + : context.lang.contextMenuPin, + ), + onSelect: () async { + final update = GroupsCompanion(pinned: Value(!widget.group.pinned)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: FaIcon( + widget.group.pinned + ? FontAwesomeIcons.thumbtackSlash + : FontAwesomeIcons.thumbtack, + ), + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/views/components/user_context_menu.component.dart b/lib/src/views/components/user_context_menu.component.dart new file mode 100644 index 0000000..46c8716 --- /dev/null +++ b/lib/src/views/components/user_context_menu.component.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; + +class UserContextMenuBlocked extends StatefulWidget { + const UserContextMenuBlocked({ + required this.contact, + required this.child, + super.key, + }); + final Widget child; + final Contact contact; + + @override + State createState() => _UserContextMenuBlocked(); +} + +class _UserContextMenuBlocked extends State { + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => (), + actions: [ + PieAction( + tooltip: Text(context.lang.contextMenuUserProfile), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(widget.contact.userId); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.user), + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart deleted file mode 100644 index ac834b3..0000000 --- a/lib/src/views/components/user_context_menu.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/contact/contact.view.dart'; - -class UserContextMenu extends StatefulWidget { - const UserContextMenu({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuState(); -} - -class _UserContextMenuState extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuOpenChat), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(widget.contact); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.solidComments), - ), - PieAction( - tooltip: Text( - widget.contact.pinned - ? context.lang.contextMenuUnpin - : context.lang.contextMenuPin, - ), - onSelect: () async { - final update = - ContactsCompanion(pinned: Value(!widget.contact.pinned)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: FaIcon( - widget.contact.pinned - ? FontAwesomeIcons.thumbtackSlash - : FontAwesomeIcons.thumbtack, - ), - ), - ], - child: widget.child, - ); - } -} - -class UserContextMenuBlocked extends StatefulWidget { - const UserContextMenuBlocked({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuBlocked(); -} - -class _UserContextMenuBlocked extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuUserProfile), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(widget.contact.userId); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.user), - ), - ], - child: widget.child, - ); - } -} - -PieTheme getPieCanvasTheme(BuildContext context) { - return PieTheme( - brightness: Theme.of(context).brightness, - rightClickShowsMenu: true, - radius: 70, - buttonTheme: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.tertiary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - buttonThemeHovered: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - tooltipPadding: const EdgeInsets.all(20), - overlayColor: isDarkMode(context) - ? const Color.fromARGB(69, 0, 0, 0) - : const Color.fromARGB(40, 0, 0, 0), - // spacing: 0, - tooltipTextStyle: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), - ); -} diff --git a/lib/src/views/components/video_player_wrapper.dart b/lib/src/views/components/video_player_wrapper.dart index b35edc8..80833d0 100644 --- a/lib/src/views/components/video_player_wrapper.dart +++ b/lib/src/views/components/video_player_wrapper.dart @@ -7,11 +7,9 @@ import 'package:video_player/video_player.dart'; class VideoPlayerWrapper extends StatefulWidget { const VideoPlayerWrapper({ required this.videoPath, - required this.mirrorVideo, super.key, }); final File videoPath; - final bool mirrorVideo; @override State createState() => _VideoPlayerWrapperState(); @@ -48,10 +46,7 @@ class _VideoPlayerWrapperState extends State { child: _controller.value.isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(_controller), - ), + child: VideoPlayer(_controller), ) : const CircularProgressIndicator(), // Show loading indicator while initializing ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index d08a914..20d6a46 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -163,26 +163,26 @@ class _ContactViewState extends State { ); }, ), - BetterListTile( - icon: FontAwesomeIcons.eraser, - iconSize: 16, - text: context.lang.deleteAllContactMessages, - onTap: () async { - final block = await showAlertDialog( - context, - context.lang.deleteAllContactMessages, - context.lang.deleteAllContactMessagesBody( - getContactDisplayName(contact), - ), - ); - if (block) { - if (context.mounted) { - await twonlyDB.messagesDao - .deleteMessagesByContactId(contact.userId); - } - } - }, - ), + // BetterListTile( + // icon: FontAwesomeIcons.eraser, + // iconSize: 16, + // text: context.lang.deleteAllContactMessages, + // onTap: () async { + // final block = await showAlertDialog( + // context, + // context.lang.deleteAllContactMessages, + // context.lang.deleteAllContactMessagesBody( + // getContactDisplayName(contact), + // ), + // ); + // if (block) { + // if (context.mounted) { + // await twonlyDB.messagesDao + // .deleteMessagesByContactId(contact.userId); + // } + // } + // }, + // ), BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 3463ea0..0c24c4a 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -10,7 +10,6 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index fd95f31..2438382 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -1,13 +1,11 @@ import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.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/mediafiles/upload.service.dart' as send; -import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.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'; @@ -24,7 +22,7 @@ class MemoriesViewState extends State { List galleryItems = []; Map> orderedByMonth = {}; List months = []; - StreamSubscription>? messageSub; + StreamSubscription>? messageSub; @override void initState() { @@ -38,76 +36,37 @@ class MemoriesViewState extends State { super.dispose(); } - Future> loadMemoriesDirectory() async { - final directoryPath = await send.getMediaBaseFilePath('memories'); - final directory = Directory(directoryPath); - - final items = []; - if (directory.existsSync()) { - final files = directory.listSync(); - - for (final file in files) { - if (file is File) { - final fileName = file.uri.pathSegments.last; - File? imagePath; - File? videoPath; - late File thumbnailFile; - if (fileName.contains('.thumbnail.')) { - continue; - } - if (fileName.contains('.png')) { - imagePath = file; - thumbnailFile = file; - // if (!await thumbnailFile.exists()) { - // await createThumbnailsForImage(imagePath); - // } - } else if (fileName.contains('.mp4')) { - videoPath = file; - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else { - break; - } - final creationDate = file.lastModifiedSync(); - items.add( - MemoryItem( - id: int.parse(fileName.split('.')[0]), - messages: [], - date: creationDate, - mirrorVideo: false, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, - ), - ); - } - } - } - return items; - } - Future initAsync() async { await messageSub?.cancel(); - final msgStream = twonlyDB.messagesDao.getAllStoredMediaFiles(); + final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles(); - messageSub = msgStream.listen((msgs) async { - final items = await MemoryItem.convertFromMessages(msgs); + messageSub = msgStream.listen((mediaFiles) async { // Group items by month orderedByMonth = {}; months = []; var lastMonth = ''; - galleryItems = await loadMemoriesDirectory(); - for (final item in galleryItems) { - items.remove( - item.id, - ); // prefer the stored one and not the saved on in the chat.... + galleryItems = []; + final applicationSupportDirectory = + await getApplicationSupportDirectory(); + for (final mediaFile in mediaFiles) { + galleryItems.add( + MemoryItem( + mediaService: MediaFileService( + mediaFile, + applicationSupportDirectory: applicationSupportDirectory, + ), + messages: [], + ), + ); } - galleryItems += items.values.toList(); - galleryItems.sort((a, b) => b.date.compareTo(a.date)); + galleryItems.sort( + (a, b) => b.mediaService.mediaFile.createdAt.compareTo( + a.mediaService.mediaFile.createdAt, + ), + ); for (var i = 0; i < galleryItems.length; i++) { - final month = DateFormat('MMMM yyyy').format(galleryItems[i].date); + final month = DateFormat('MMMM yyyy') + .format(galleryItems[i].mediaService.mediaFile.createdAt); if (lastMonth != month) { lastMonth = month; months.add(month); diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index efcd0f6..cab777e 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; class MemoriesItemThumbnail extends StatefulWidget { @@ -39,11 +40,12 @@ class _MemoriesItemThumbnailState extends State { return GestureDetector( onTap: widget.onTap, child: Hero( - tag: widget.galleryItem.id.toString(), + tag: widget.galleryItem.mediaService.mediaFile.mediaId, child: Stack( children: [ - Image.file(widget.galleryItem.thumbnailPath), - if (widget.galleryItem.videoPath != null) + Image.file(widget.galleryItem.mediaService.thumbnailPath), + if (widget.galleryItem.mediaService.mediaFile.type == + MediaType.video) const Positioned.fill( child: Center( child: FaIcon(FontAwesomeIcons.circlePlay), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index bb2abf5..5d9f1b7 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -1,14 +1,10 @@ -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:photo_view/photo_view.dart'; 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/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -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'; @@ -51,7 +47,6 @@ class _MemoriesPhotoSliderViewState extends State { } Future deleteFile() async { - final messages = widget.galleryItems[currentIndex].messages; final confirmed = await showAlertDialog( context, context.lang.deleteImageTitle, @@ -60,32 +55,26 @@ class _MemoriesPhotoSliderViewState extends State { if (!confirmed) return; - widget.galleryItems[currentIndex].imagePath?.deleteSync(); - widget.galleryItems[currentIndex].videoPath?.deleteSync(); - for (final message in messages) { - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion(mediaStored: Value(false)), - ); - } + widget.galleryItems[currentIndex].mediaService.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile( + widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId, + ); widget.galleryItems.removeAt(currentIndex); setState(() {}); - await send.purgeSendMediaFiles(); - await received.purgeReceivedMediaFiles(); if (mounted) { Navigator.pop(context, true); } } Future exportFile() async { - final item = widget.galleryItems[currentIndex]; + final item = widget.galleryItems[currentIndex].mediaService; try { - if (item.videoPath != null) { - await saveVideoToGallery(item.videoPath!.path); - } else if (item.imagePath != null) { - final imageBytes = await item.imagePath!.readAsBytes(); + if (item.mediaFile.type == MediaType.video) { + await saveVideoToGallery(item.storedPath.path); + } else if (item.mediaFile.type == MediaType.image) { + final imageBytes = await item.storedPath.readAsBytes(); await saveImageToGallery(imageBytes); } if (!mounted) return; @@ -132,14 +121,9 @@ class _MemoriesPhotoSliderViewState extends State { context, MaterialPageRoute( builder: (context) => ShareImageEditorView( - videoFilePath: - widget.galleryItems[currentIndex].videoPath, - imageBytes: widget - .galleryItems[currentIndex].imagePath - ?.readAsBytes(), - mirrorVideo: false, + mediaFileService: widget + .galleryItems[currentIndex].mediaService, sharedFromGallery: true, - useHighQuality: true, ), ), ); @@ -214,24 +198,25 @@ class _MemoriesPhotoSliderViewState extends State { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { final item = widget.galleryItems[index]; - return item.videoPath != null + return item.mediaService.mediaFile.type == MediaType.video ? PhotoViewGalleryPageOptions.customChild( child: VideoPlayerWrapper( - videoPath: item.videoPath!, - mirrorVideo: item.mirrorVideo, + videoPath: item.mediaService.storedPath, ), // childSize: const Size(300, 300), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId), ) : PhotoViewGalleryPageOptions( - imageProvider: FileImage(item.imagePath!), + imageProvider: FileImage(item.mediaService.storedPath), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId), ); } } diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 394f5e3..01c2b17 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -6,7 +6,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; class PrivacyViewBlockUsers extends StatefulWidget { const PrivacyViewBlockUsers({super.key}); diff --git a/test/unit_test.dart b/test/unit_test.dart index ffe3b19..603e6ed 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,10 +1,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; - import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.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'; @@ -22,15 +20,6 @@ void main() { expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33); }); - test('test utils', () async { - final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); - final list2 = Uint8List.fromList([42, 42, 42]); - final combined = combineUint8Lists(list1, list2); - final lists = extractUint8Lists(combined); - expect(list1, lists[0]); - expect(list2, lists[1]); - }); - test('encode hex', () async { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1)));