diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ecc08d..7a6df866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.2.12 + +- Improved: Memories viewer redesigned with smoother animations and new quick-action controls. + ## 0.2.11 - New: Create custom shortcuts to quickly share images with pre-selected groups diff --git a/lib/globals.dart b/lib/globals.dart index c4caaec2..41de888a 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -9,8 +9,6 @@ class AppEnvironment { static bool _isInitialized = false; - static bool _isInitialized = false; - // will be loaded in the main_camera_controller.dart static List cameras = []; @@ -34,5 +32,6 @@ class AppState { static bool isInBackgroundTask = false; static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; - static int latestAppVersionId = 113; + static int latestAppVersionId = 114; + } diff --git a/lib/main.dart b/lib/main.dart index 77f5db8d..e2e94029 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; import 'package:mutex/mutex.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -17,6 +19,7 @@ import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart' show getSignalSignedPreKeyStoreOld; import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; @@ -28,6 +31,8 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/memories/memories.service.dart'; + import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/user.service.dart'; @@ -247,9 +252,24 @@ Future runMigrations() async { }); } } + if (userService.currentUser.appVersion < 114) { + final allMedia = await twonlyDB.mediaFilesDao + .select(twonlyDB.mediaFiles) + .get(); + for (final media in allMedia) { + if (media.createdAtMonth == null) { + final monthStr = DateFormat('MMMM yyyy').format(media.createdAt); + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + MediaFilesCompanion(createdAtMonth: Value(monthStr)), + ); + } + } + await UserService.update((u) => u.appVersion = 114); + } if (kDebugMode) { assert( - AppState.latestAppVersionId == 113, + AppState.latestAppVersionId == 114, 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', ); assert( @@ -261,6 +281,8 @@ Future runMigrations() async { Future postStartupTasks() async { Log.info('Post startup started.'); + unawaited(MemoriesService.prewarmCache()); + // 1. Immediate background cleanup (Non-blocking for UI) await twonlyDB.messagesDao.purgeMessageTable(); unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts()); diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 3afade5f..ec8a71c8 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -122,6 +122,13 @@ class MediaFilesDao extends DatabaseAccessor .get(); } + Future> getAllUnanalyzedStoredMediaFiles() async { + return (select(mediaFiles)..where( + (t) => t.stored.equals(true) & t.hasCropAnalyzed.equals(false), + )) + .get(); + } + Future> getAllMediaFilesPendingUpload() async { return (select(mediaFiles)..where( (t) => diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v14.json b/lib/src/database/schemas/twonly_db/drift_schema_v14.json new file mode 100644 index 00000000..7790a9a0 --- /dev/null +++ b/lib/src/database/schemas/twonly_db/drift_schema_v14.json @@ -0,0 +1,2939 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "contacts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "nick_name", + "getter_name": "nickName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_svg_compressed", + "getter_name": "avatarSvgCompressed", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_profile_counter", + "getter_name": "senderProfileCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "accepted", + "getter_name": "accepted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"accepted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"accepted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_by_user", + "getter_name": "deletedByUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_by_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_by_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "requested", + "getter_name": "requested", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requested\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requested\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "blocked", + "getter_name": "blocked", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"blocked\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"blocked\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "verified", + "getter_name": "verified", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"verified\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"verified\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_deleted", + "getter_name": "accountDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"account_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"account_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_version", + "getter_name": "userDiscoveryVersion", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_excluded", + "getter_name": "userDiscoveryExcluded", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_excluded\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_excluded\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_manual_approved", + "getter_name": "userDiscoveryManualApproved", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_send_counter", + "getter_name": "mediaSendCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_received_counter", + "getter_name": "mediaReceivedCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "user_id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "groups", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_group_admin", + "getter_name": "isGroupAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_group_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_group_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_direct_chat", + "getter_name": "isDirectChat", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_direct_chat\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_direct_chat\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "getter_name": "pinned", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"pinned\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"pinned\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "archived", + "getter_name": "archived", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"archived\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"archived\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "joined_group", + "getter_name": "joinedGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"joined_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"joined_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "left_group", + "getter_name": "leftGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"left_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"left_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_content", + "getter_name": "deletedContent", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_content\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_content\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_version_id", + "getter_name": "stateVersionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_encryption_key", + "getter_name": "stateEncryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "my_group_private_key", + "getter_name": "myGroupPrivateKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_name", + "getter_name": "groupName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "draft_message", + "getter_name": "draftMessage", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total_media_counter", + "getter_name": "totalMediaCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "also_best_friend", + "getter_name": "alsoBestFriend", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"also_best_friend\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"also_best_friend\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "delete_messages_after_milliseconds", + "getter_name": "deleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('86400000')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_send", + "getter_name": "lastMessageSend", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_received", + "getter_name": "lastMessageReceived", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_counter_change", + "getter_name": "lastFlameCounterChange", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_sync", + "getter_name": "lastFlameSync", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "flame_counter", + "getter_name": "flameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter", + "getter_name": "maxFlameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter_from", + "getter_name": "maxFlameCounterFrom", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_exchange", + "getter_name": "lastMessageExchange", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "media_files", + "was_declared_in_moor": false, + "columns": [ + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MediaType.values)", + "dart_type_name": "MediaType" + } + }, + { + "name": "upload_state", + "getter_name": "uploadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(UploadState.values)", + "dart_type_name": "UploadState" + } + }, + { + "name": "download_state", + "getter_name": "downloadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(DownloadState.values)", + "dart_type_name": "DownloadState" + } + }, + { + "name": "requires_authentication", + "getter_name": "requiresAuthentication", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requires_authentication\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requires_authentication\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored", + "getter_name": "stored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft_media", + "getter_name": "isDraftMedia", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft_media\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft_media\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_crop_analyzed", + "getter_name": "hasCropAnalyzed", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_crop_analyzed\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_crop_analyzed\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_progressing_process", + "getter_name": "preProgressingProcess", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reupload_requested_by", + "getter_name": "reuploadRequestedBy", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "IntListTypeConverter()", + "dart_type_name": "List" + } + }, + { + "name": "display_limit_in_milliseconds", + "getter_name": "displayLimitInMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "remove_audio", + "getter_name": "removeAudio", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"remove_audio\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"remove_audio\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_key", + "getter_name": "encryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_mac", + "getter_name": "encryptionMac", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_nonce", + "getter_name": "encryptionNonce", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored_file_hash", + "getter_name": "storedFileHash", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at_month", + "getter_name": "createdAtMonth", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "media_id" + ] + } + }, + { + "id": 3, + "references": [ + 1, + 0, + 2 + ], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES media_files (media_id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES media_files (media_id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "media_files", + "column": "media_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "additional_message_data", + "getter_name": "additionalMessageData", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_stored", + "getter_name": "mediaStored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_reopened", + "getter_name": "mediaReopened", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_reopened\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_reopened\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quotes_message_id", + "getter_name": "quotesMessageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted_from_sender", + "getter_name": "isDeletedFromSender", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted_from_sender\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted_from_sender\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_at", + "getter_name": "openedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_by_all", + "getter_name": "openedByAll", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "modified_at", + "getter_name": "modifiedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_user", + "getter_name": "ackByUser", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server", + "getter_name": "ackByServer", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id" + ] + } + }, + { + "id": 4, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "sender_id", + "emoji" + ] + } + }, + { + "id": 6, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "member_state", + "getter_name": "memberState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MemberState.values)", + "dart_type_name": "MemberState" + } + }, + { + "name": "group_public_key", + "getter_name": "groupPublicKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_chat_opened", + "getter_name": "lastChatOpened", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_type_indicator", + "getter_name": "lastTypeIndicator", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message", + "getter_name": "lastMessage", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id", + "contact_id" + ] + } + }, + { + "id": 7, + "references": [ + 0, + 3 + ], + "type": "table", + "data": { + "name": "receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message", + "getter_name": "message", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_will_sends_receipt", + "getter_name": "contactWillSendsReceipt", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "will_be_retried_by_media_upload", + "getter_name": "willBeRetriedByMediaUpload", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry", + "getter_name": "markForRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry_after_accepted", + "getter_name": "markForRetryAfterAccepted", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server_at", + "getter_name": "ackByServerAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "retry_count", + "getter_name": "retryCount", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_retry", + "getter_name": "lastRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "received_receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "signal_identity_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "identity_key", + "getter_name": "identityKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 10, + "references": [], + "type": "table", + "data": { + "name": "signal_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "pre_key_id", + "getter_name": "preKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_key", + "getter_name": "preKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "pre_key_id" + ] + } + }, + { + "id": 11, + "references": [], + "type": "table", + "data": { + "name": "signal_sender_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "sender_key_name", + "getter_name": "senderKeyName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_key", + "getter_name": "senderKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "sender_key_name" + ] + } + }, + { + "id": 12, + "references": [], + "type": "table", + "data": { + "name": "signal_session_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "session_record", + "getter_name": "sessionRecord", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 13, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_actions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MessageActionType.values)", + "dart_type_name": "MessageActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "contact_id", + "type" + ] + } + }, + { + "id": 14, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_history_id", + "getter_name": "groupHistoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "affected_contact_id", + "getter_name": "affectedContactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "old_group_name", + "getter_name": "oldGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_group_name", + "getter_name": "newGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_delete_messages_after_milliseconds", + "getter_name": "newDeleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GroupActionType.values)", + "dart_type_name": "GroupActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_history_id" + ] + } + }, + { + "id": 15, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "key_verifications", + "was_declared_in_moor": false, + "columns": [ + { + "name": "verification_id", + "getter_name": "verificationId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(VerificationType.values)", + "dart_type_name": "VerificationType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 16, + "references": [], + "type": "table", + "data": { + "name": "verification_tokens", + "was_declared_in_moor": false, + "columns": [ + { + "name": "token_id", + "getter_name": "tokenId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 17, + "references": [], + "type": "table", + "data": { + "name": "user_discovery_announced_users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announced_public_key", + "getter_name": "announcedPublicKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_shown_to_the_user", + "getter_name": "wasShownToTheUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_shown_to_the_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_shown_to_the_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id" + ] + } + }, + { + "id": 18, + "references": [ + 17, + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_user_relations", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_discovery_announced_users", + "column": "announced_user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id", + "from_contact_id" + ] + } + }, + { + "id": 19, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_other_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion_id", + "getter_name": "promotionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "threshold", + "getter_name": "threshold", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announcement_share", + "getter_name": "announcementShare", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "from_contact_id", + "public_id" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_own_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "version_id", + "getter_name": "versionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion", + "getter_name": "promotion", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 21, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_shares", + "was_declared_in_moor": false, + "columns": [ + { + "name": "share_id", + "getter_name": "shareId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "share", + "getter_name": "share", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 22, + "references": [], + "type": "table", + "data": { + "name": "shortcuts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "usage_counter", + "getter_name": "usageCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 23, + "references": [ + 22, + 1 + ], + "type": "table", + "data": { + "name": "shortcut_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shortcut_id", + "getter_name": "shortcutId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES shortcuts (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES shortcuts (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "shortcuts", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "shortcut_id", + "group_id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "contacts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + } + ] + }, + { + "name": "groups", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"group_id\" TEXT NOT NULL, \"is_group_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_group_admin\" IN (0, 1)), \"is_direct_chat\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_direct_chat\" IN (0, 1)), \"pinned\" INTEGER NOT NULL DEFAULT 0 CHECK (\"pinned\" IN (0, 1)), \"archived\" INTEGER NOT NULL DEFAULT 0 CHECK (\"archived\" IN (0, 1)), \"joined_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"joined_group\" IN (0, 1)), \"left_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"left_group\" IN (0, 1)), \"deleted_content\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_content\" IN (0, 1)), \"state_version_id\" INTEGER NOT NULL DEFAULT 0, \"state_encryption_key\" BLOB NULL, \"my_group_private_key\" BLOB NULL, \"group_name\" TEXT NOT NULL, \"draft_message\" TEXT NULL, \"total_media_counter\" INTEGER NOT NULL DEFAULT 0, \"also_best_friend\" INTEGER NOT NULL DEFAULT 0 CHECK (\"also_best_friend\" IN (0, 1)), \"delete_messages_after_milliseconds\" INTEGER NOT NULL DEFAULT 86400000, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"last_message_send\" INTEGER NULL, \"last_message_received\" INTEGER NULL, \"last_flame_counter_change\" INTEGER NULL, \"last_flame_sync\" INTEGER NULL, \"flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter_from\" INTEGER NULL, \"last_message_exchange\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\"));" + } + ] + }, + { + "name": "media_files", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"media_files\" (\"media_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"upload_state\" TEXT NULL, \"download_state\" TEXT NULL, \"requires_authentication\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requires_authentication\" IN (0, 1)), \"stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"stored\" IN (0, 1)), \"is_draft_media\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft_media\" IN (0, 1)), \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"has_crop_analyzed\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_crop_analyzed\" IN (0, 1)), \"pre_progressing_process\" INTEGER NULL, \"reupload_requested_by\" TEXT NULL, \"display_limit_in_milliseconds\" INTEGER NULL, \"remove_audio\" INTEGER NULL CHECK (\"remove_audio\" IN (0, 1)), \"download_token\" BLOB NULL, \"encryption_key\" BLOB NULL, \"encryption_mac\" BLOB NULL, \"encryption_nonce\" BLOB NULL, \"stored_file_hash\" BLOB NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"created_at_month\" TEXT NULL, PRIMARY KEY (\"media_id\"));" + } + ] + }, + { + "name": "messages", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"messages\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"message_id\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id), \"type\" TEXT NOT NULL, \"content\" TEXT NULL, \"media_id\" TEXT NULL REFERENCES media_files (media_id) ON DELETE SET NULL, \"additional_message_data\" BLOB NULL, \"media_stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_stored\" IN (0, 1)), \"media_reopened\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_reopened\" IN (0, 1)), \"download_token\" BLOB NULL, \"quotes_message_id\" TEXT NULL, \"is_deleted_from_sender\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted_from_sender\" IN (0, 1)), \"opened_at\" INTEGER NULL, \"opened_by_all\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"modified_at\" INTEGER NULL, \"ack_by_user\" INTEGER NULL, \"ack_by_server\" INTEGER NULL, PRIMARY KEY (\"message_id\"));" + } + ] + }, + { + "name": "message_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_histories\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"content\" TEXT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "reactions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"reactions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"emoji\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"sender_id\", \"emoji\"));" + } + ] + }, + { + "name": "group_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id), \"member_state\" TEXT NULL, \"group_public_key\" BLOB NULL, \"last_chat_opened\" INTEGER NULL, \"last_type_indicator\" INTEGER NULL, \"last_message\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\", \"contact_id\"));" + } + ] + }, + { + "name": "receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"receipts\" (\"receipt_id\" TEXT NOT NULL, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"message_id\" TEXT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"message\" BLOB NOT NULL, \"contact_will_sends_receipt\" INTEGER NOT NULL DEFAULT 1 CHECK (\"contact_will_sends_receipt\" IN (0, 1)), \"will_be_retried_by_media_upload\" INTEGER NOT NULL DEFAULT 0 CHECK (\"will_be_retried_by_media_upload\" IN (0, 1)), \"mark_for_retry\" INTEGER NULL, \"mark_for_retry_after_accepted\" INTEGER NULL, \"ack_by_server_at\" INTEGER NULL, \"retry_count\" INTEGER NOT NULL DEFAULT 0, \"last_retry\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "received_receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"received_receipts\" (\"receipt_id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "signal_identity_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_identity_key_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"identity_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_pre_key_stores\" (\"pre_key_id\" INTEGER NOT NULL, \"pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"pre_key_id\"));" + } + ] + }, + { + "name": "signal_sender_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_sender_key_stores\" (\"sender_key_name\" TEXT NOT NULL, \"sender_key\" BLOB NOT NULL, PRIMARY KEY (\"sender_key_name\"));" + } + ] + }, + { + "name": "signal_session_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_session_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"session_record\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "message_actions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_actions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"contact_id\", \"type\"));" + } + ] + }, + { + "name": "group_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_histories\" (\"group_history_id\" TEXT NOT NULL, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id), \"affected_contact_id\" INTEGER NULL, \"old_group_name\" TEXT NULL, \"new_group_name\" TEXT NULL, \"new_delete_messages_after_milliseconds\" INTEGER NULL, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_history_id\"));" + } + ] + }, + { + "name": "key_verifications", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"key_verifications\" (\"verification_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "verification_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"verification_tokens\" (\"token_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "user_discovery_announced_users", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_announced_users\" (\"announced_user_id\" INTEGER NOT NULL, \"announced_public_key\" BLOB NOT NULL, \"public_id\" INTEGER NOT NULL UNIQUE, \"username\" TEXT NULL, \"was_shown_to_the_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_shown_to_the_user\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_hidden\" IN (0, 1)), PRIMARY KEY (\"announced_user_id\"));" + } + ] + }, + { + "name": "user_discovery_user_relations", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_user_relations\" (\"announced_user_id\" INTEGER NOT NULL REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE, \"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"announced_user_id\", \"from_contact_id\"));" + } + ] + }, + { + "name": "user_discovery_other_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_other_promotions\" (\"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion_id\" INTEGER NOT NULL, \"public_id\" INTEGER NOT NULL, \"threshold\" INTEGER NOT NULL, \"announcement_share\" BLOB NOT NULL, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"from_contact_id\", \"public_id\"));" + } + ] + }, + { + "name": "user_discovery_own_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_own_promotions\" (\"version_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion\" BLOB NOT NULL);" + } + ] + }, + { + "name": "user_discovery_shares", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_shares\" (\"share_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"share\" BLOB NOT NULL, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE);" + } + ] + }, + { + "name": "shortcuts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcuts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"emoji\" TEXT NOT NULL UNIQUE, \"usage_counter\" INTEGER NOT NULL DEFAULT 0);" + } + ] + }, + { + "name": "shortcut_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcut_members\" (\"shortcut_id\" INTEGER NOT NULL REFERENCES shortcuts (id) ON DELETE CASCADE, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, PRIMARY KEY (\"shortcut_id\", \"group_id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 2cbb45c5..a7c16f96 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -50,6 +50,9 @@ class MediaFiles extends Table { BoolColumn get stored => boolean().withDefault(const Constant(false))(); BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + BoolColumn get hasCropAnalyzed => + boolean().withDefault(const Constant(false))(); IntColumn get preProgressingProcess => integer().nullable()(); @@ -67,6 +70,8 @@ class MediaFiles extends Table { BlobColumn get storedFileHash => blob().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + TextColumn get createdAtMonth => text().nullable()(); + @override Set get primaryKey => {mediaId}; diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index bba50b61..c39ef370 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -79,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB { TwonlyDB.forTesting(DatabaseConnection super.connection); @override - int get schemaVersion => 13; + int get schemaVersion => 14; static QueryExecutor _openConnection() { return driftDatabase( @@ -195,6 +195,17 @@ class TwonlyDB extends _$TwonlyDB { await m.createTable(schema.shortcuts); await m.createTable(schema.shortcutMembers); }, + from13To14: (m, schema) async { + await m.addColumn( + schema.mediaFiles, + schema.mediaFiles.createdAtMonth, + ); + await m.addColumn(schema.mediaFiles, schema.mediaFiles.isFavorite); + await m.addColumn( + schema.mediaFiles, + schema.mediaFiles.hasCropAnalyzed, + ); + }, )(m, from, to); }, ); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 2d241408..4ef1df3b 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2676,6 +2676,36 @@ class $MediaFilesTable extends MediaFiles ), defaultValue: const Constant(false), ); + static const VerificationMeta _isFavoriteMeta = const VerificationMeta( + 'isFavorite', + ); + @override + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _hasCropAnalyzedMeta = const VerificationMeta( + 'hasCropAnalyzed', + ); + @override + late final GeneratedColumn hasCropAnalyzed = GeneratedColumn( + 'has_crop_analyzed', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_crop_analyzed" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); static const VerificationMeta _preProgressingProcessMeta = const VerificationMeta('preProgressingProcess'); @override @@ -2792,6 +2822,17 @@ class $MediaFilesTable extends MediaFiles requiredDuringInsert: false, defaultValue: currentDateAndTime, ); + static const VerificationMeta _createdAtMonthMeta = const VerificationMeta( + 'createdAtMonth', + ); + @override + late final GeneratedColumn createdAtMonth = GeneratedColumn( + 'created_at_month', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override List get $columns => [ mediaId, @@ -2801,6 +2842,8 @@ class $MediaFilesTable extends MediaFiles requiresAuthentication, stored, isDraftMedia, + isFavorite, + hasCropAnalyzed, preProgressingProcess, reuploadRequestedBy, displayLimitInMilliseconds, @@ -2811,6 +2854,7 @@ class $MediaFilesTable extends MediaFiles encryptionNonce, storedFileHash, createdAt, + createdAtMonth, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2856,6 +2900,21 @@ class $MediaFilesTable extends MediaFiles ), ); } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown(data['is_favorite']!, _isFavoriteMeta), + ); + } + if (data.containsKey('has_crop_analyzed')) { + context.handle( + _hasCropAnalyzedMeta, + hasCropAnalyzed.isAcceptableOrUnknown( + data['has_crop_analyzed']!, + _hasCropAnalyzedMeta, + ), + ); + } if (data.containsKey('pre_progressing_process')) { context.handle( _preProgressingProcessMeta, @@ -2934,6 +2993,15 @@ class $MediaFilesTable extends MediaFiles createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), ); } + if (data.containsKey('created_at_month')) { + context.handle( + _createdAtMonthMeta, + createdAtMonth.isAcceptableOrUnknown( + data['created_at_month']!, + _createdAtMonthMeta, + ), + ); + } return context; } @@ -2977,6 +3045,14 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.bool, data['${effectivePrefix}is_draft_media'], )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + hasCropAnalyzed: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_crop_analyzed'], + )!, preProgressingProcess: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}pre_progressing_process'], @@ -3020,6 +3096,10 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.dateTime, data['${effectivePrefix}created_at'], )!, + createdAtMonth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_month'], + ), ); } @@ -3056,6 +3136,8 @@ class MediaFile extends DataClass implements Insertable { final bool requiresAuthentication; final bool stored; final bool isDraftMedia; + final bool isFavorite; + final bool hasCropAnalyzed; final int? preProgressingProcess; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; @@ -3066,6 +3148,7 @@ class MediaFile extends DataClass implements Insertable { final Uint8List? encryptionNonce; final Uint8List? storedFileHash; final DateTime createdAt; + final String? createdAtMonth; const MediaFile({ required this.mediaId, required this.type, @@ -3074,6 +3157,8 @@ class MediaFile extends DataClass implements Insertable { required this.requiresAuthentication, required this.stored, required this.isDraftMedia, + required this.isFavorite, + required this.hasCropAnalyzed, this.preProgressingProcess, this.reuploadRequestedBy, this.displayLimitInMilliseconds, @@ -3084,6 +3169,7 @@ class MediaFile extends DataClass implements Insertable { this.encryptionNonce, this.storedFileHash, required this.createdAt, + this.createdAtMonth, }); @override Map toColumns(bool nullToAbsent) { @@ -3107,6 +3193,8 @@ class MediaFile extends DataClass implements Insertable { map['requires_authentication'] = Variable(requiresAuthentication); map['stored'] = Variable(stored); map['is_draft_media'] = Variable(isDraftMedia); + map['is_favorite'] = Variable(isFavorite); + map['has_crop_analyzed'] = Variable(hasCropAnalyzed); if (!nullToAbsent || preProgressingProcess != null) { map['pre_progressing_process'] = Variable(preProgressingProcess); } @@ -3141,6 +3229,9 @@ class MediaFile extends DataClass implements Insertable { map['stored_file_hash'] = Variable(storedFileHash); } map['created_at'] = Variable(createdAt); + if (!nullToAbsent || createdAtMonth != null) { + map['created_at_month'] = Variable(createdAtMonth); + } return map; } @@ -3157,6 +3248,8 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: Value(requiresAuthentication), stored: Value(stored), isDraftMedia: Value(isDraftMedia), + isFavorite: Value(isFavorite), + hasCropAnalyzed: Value(hasCropAnalyzed), preProgressingProcess: preProgressingProcess == null && nullToAbsent ? const Value.absent() : Value(preProgressingProcess), @@ -3186,6 +3279,9 @@ class MediaFile extends DataClass implements Insertable { ? const Value.absent() : Value(storedFileHash), createdAt: Value(createdAt), + createdAtMonth: createdAtMonth == null && nullToAbsent + ? const Value.absent() + : Value(createdAtMonth), ); } @@ -3210,6 +3306,8 @@ class MediaFile extends DataClass implements Insertable { ), stored: serializer.fromJson(json['stored']), isDraftMedia: serializer.fromJson(json['isDraftMedia']), + isFavorite: serializer.fromJson(json['isFavorite']), + hasCropAnalyzed: serializer.fromJson(json['hasCropAnalyzed']), preProgressingProcess: serializer.fromJson( json['preProgressingProcess'], ), @@ -3226,6 +3324,7 @@ class MediaFile extends DataClass implements Insertable { encryptionNonce: serializer.fromJson(json['encryptionNonce']), storedFileHash: serializer.fromJson(json['storedFileHash']), createdAt: serializer.fromJson(json['createdAt']), + createdAtMonth: serializer.fromJson(json['createdAtMonth']), ); } @override @@ -3245,6 +3344,8 @@ class MediaFile extends DataClass implements Insertable { 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'stored': serializer.toJson(stored), 'isDraftMedia': serializer.toJson(isDraftMedia), + 'isFavorite': serializer.toJson(isFavorite), + 'hasCropAnalyzed': serializer.toJson(hasCropAnalyzed), 'preProgressingProcess': serializer.toJson(preProgressingProcess), 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson( @@ -3257,6 +3358,7 @@ class MediaFile extends DataClass implements Insertable { 'encryptionNonce': serializer.toJson(encryptionNonce), 'storedFileHash': serializer.toJson(storedFileHash), 'createdAt': serializer.toJson(createdAt), + 'createdAtMonth': serializer.toJson(createdAtMonth), }; } @@ -3268,6 +3370,8 @@ class MediaFile extends DataClass implements Insertable { bool? requiresAuthentication, bool? stored, bool? isDraftMedia, + bool? isFavorite, + bool? hasCropAnalyzed, Value preProgressingProcess = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), @@ -3278,6 +3382,7 @@ class MediaFile extends DataClass implements Insertable { Value encryptionNonce = const Value.absent(), Value storedFileHash = const Value.absent(), DateTime? createdAt, + Value createdAtMonth = const Value.absent(), }) => MediaFile( mediaId: mediaId ?? this.mediaId, type: type ?? this.type, @@ -3289,6 +3394,8 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication ?? this.requiresAuthentication, stored: stored ?? this.stored, isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, preProgressingProcess: preProgressingProcess.present ? preProgressingProcess.value : this.preProgressingProcess, @@ -3315,6 +3422,9 @@ class MediaFile extends DataClass implements Insertable { ? storedFileHash.value : this.storedFileHash, createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth.present + ? createdAtMonth.value + : this.createdAtMonth, ); MediaFile copyWithCompanion(MediaFilesCompanion data) { return MediaFile( @@ -3333,6 +3443,12 @@ class MediaFile extends DataClass implements Insertable { isDraftMedia: data.isDraftMedia.present ? data.isDraftMedia.value : this.isDraftMedia, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + hasCropAnalyzed: data.hasCropAnalyzed.present + ? data.hasCropAnalyzed.value + : this.hasCropAnalyzed, preProgressingProcess: data.preProgressingProcess.present ? data.preProgressingProcess.value : this.preProgressingProcess, @@ -3361,6 +3477,9 @@ class MediaFile extends DataClass implements Insertable { ? data.storedFileHash.value : this.storedFileHash, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + createdAtMonth: data.createdAtMonth.present + ? data.createdAtMonth.value + : this.createdAtMonth, ); } @@ -3374,6 +3493,8 @@ class MediaFile extends DataClass implements Insertable { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') ..write('preProgressingProcess: $preProgressingProcess, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') @@ -3383,7 +3504,8 @@ class MediaFile extends DataClass implements Insertable { ..write('encryptionMac: $encryptionMac, ') ..write('encryptionNonce: $encryptionNonce, ') ..write('storedFileHash: $storedFileHash, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth') ..write(')')) .toString(); } @@ -3397,6 +3519,8 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication, stored, isDraftMedia, + isFavorite, + hasCropAnalyzed, preProgressingProcess, reuploadRequestedBy, displayLimitInMilliseconds, @@ -3407,6 +3531,7 @@ class MediaFile extends DataClass implements Insertable { $driftBlobEquality.hash(encryptionNonce), $driftBlobEquality.hash(storedFileHash), createdAt, + createdAtMonth, ); @override bool operator ==(Object other) => @@ -3419,6 +3544,8 @@ class MediaFile extends DataClass implements Insertable { other.requiresAuthentication == this.requiresAuthentication && other.stored == this.stored && other.isDraftMedia == this.isDraftMedia && + other.isFavorite == this.isFavorite && + other.hasCropAnalyzed == this.hasCropAnalyzed && other.preProgressingProcess == this.preProgressingProcess && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && @@ -3434,7 +3561,8 @@ class MediaFile extends DataClass implements Insertable { other.storedFileHash, this.storedFileHash, ) && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.createdAtMonth == this.createdAtMonth); } class MediaFilesCompanion extends UpdateCompanion { @@ -3445,6 +3573,8 @@ class MediaFilesCompanion extends UpdateCompanion { final Value requiresAuthentication; final Value stored; final Value isDraftMedia; + final Value isFavorite; + final Value hasCropAnalyzed; final Value preProgressingProcess; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; @@ -3455,6 +3585,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value encryptionNonce; final Value storedFileHash; final Value createdAt; + final Value createdAtMonth; final Value rowid; const MediaFilesCompanion({ this.mediaId = const Value.absent(), @@ -3464,6 +3595,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), this.preProgressingProcess = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), @@ -3474,6 +3607,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.storedFileHash = const Value.absent(), this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), }); MediaFilesCompanion.insert({ @@ -3484,6 +3618,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), this.preProgressingProcess = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), @@ -3494,6 +3630,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.storedFileHash = const Value.absent(), this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), }) : mediaId = Value(mediaId), type = Value(type); @@ -3505,6 +3642,8 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? requiresAuthentication, Expression? stored, Expression? isDraftMedia, + Expression? isFavorite, + Expression? hasCropAnalyzed, Expression? preProgressingProcess, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, @@ -3515,6 +3654,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? encryptionNonce, Expression? storedFileHash, Expression? createdAt, + Expression? createdAtMonth, Expression? rowid, }) { return RawValuesInsertable({ @@ -3526,6 +3666,8 @@ class MediaFilesCompanion extends UpdateCompanion { 'requires_authentication': requiresAuthentication, if (stored != null) 'stored': stored, if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (isFavorite != null) 'is_favorite': isFavorite, + if (hasCropAnalyzed != null) 'has_crop_analyzed': hasCropAnalyzed, if (preProgressingProcess != null) 'pre_progressing_process': preProgressingProcess, if (reuploadRequestedBy != null) @@ -3539,6 +3681,7 @@ class MediaFilesCompanion extends UpdateCompanion { if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, if (storedFileHash != null) 'stored_file_hash': storedFileHash, if (createdAt != null) 'created_at': createdAt, + if (createdAtMonth != null) 'created_at_month': createdAtMonth, if (rowid != null) 'rowid': rowid, }); } @@ -3551,6 +3694,8 @@ class MediaFilesCompanion extends UpdateCompanion { Value? requiresAuthentication, Value? stored, Value? isDraftMedia, + Value? isFavorite, + Value? hasCropAnalyzed, Value? preProgressingProcess, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, @@ -3561,6 +3706,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? encryptionNonce, Value? storedFileHash, Value? createdAt, + Value? createdAtMonth, Value? rowid, }) { return MediaFilesCompanion( @@ -3572,6 +3718,8 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication ?? this.requiresAuthentication, stored: stored ?? this.stored, isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, preProgressingProcess: preProgressingProcess ?? this.preProgressingProcess, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, @@ -3584,6 +3732,7 @@ class MediaFilesCompanion extends UpdateCompanion { encryptionNonce: encryptionNonce ?? this.encryptionNonce, storedFileHash: storedFileHash ?? this.storedFileHash, createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth ?? this.createdAtMonth, rowid: rowid ?? this.rowid, ); } @@ -3620,6 +3769,12 @@ class MediaFilesCompanion extends UpdateCompanion { if (isDraftMedia.present) { map['is_draft_media'] = Variable(isDraftMedia.value); } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (hasCropAnalyzed.present) { + map['has_crop_analyzed'] = Variable(hasCropAnalyzed.value); + } if (preProgressingProcess.present) { map['pre_progressing_process'] = Variable( preProgressingProcess.value, @@ -3658,6 +3813,9 @@ class MediaFilesCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (createdAtMonth.present) { + map['created_at_month'] = Variable(createdAtMonth.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -3674,6 +3832,8 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') ..write('preProgressingProcess: $preProgressingProcess, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') @@ -3684,6 +3844,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('encryptionNonce: $encryptionNonce, ') ..write('storedFileHash: $storedFileHash, ') ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -14890,6 +15051,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder = Value requiresAuthentication, Value stored, Value isDraftMedia, + Value isFavorite, + Value hasCropAnalyzed, Value preProgressingProcess, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, @@ -14900,6 +15063,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = Value encryptionNonce, Value storedFileHash, Value createdAt, + Value createdAtMonth, Value rowid, }); typedef $$MediaFilesTableUpdateCompanionBuilder = @@ -14911,6 +15075,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = Value requiresAuthentication, Value stored, Value isDraftMedia, + Value isFavorite, + Value hasCropAnalyzed, Value preProgressingProcess, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, @@ -14921,6 +15087,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = Value encryptionNonce, Value storedFileHash, Value createdAt, + Value createdAtMonth, Value rowid, }); @@ -14994,6 +15161,16 @@ class $$MediaFilesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get hasCropAnalyzed => $composableBuilder( + column: $table.hasCropAnalyzed, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get preProgressingProcess => $composableBuilder( column: $table.preProgressingProcess, builder: (column) => ColumnFilters(column), @@ -15045,6 +15222,11 @@ class $$MediaFilesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get createdAtMonth => $composableBuilder( + column: $table.createdAtMonth, + builder: (column) => ColumnFilters(column), + ); + Expression messagesRefs( Expression Function($$MessagesTableFilterComposer f) f, ) { @@ -15115,6 +15297,16 @@ class $$MediaFilesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get hasCropAnalyzed => $composableBuilder( + column: $table.hasCropAnalyzed, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get preProgressingProcess => $composableBuilder( column: $table.preProgressingProcess, builder: (column) => ColumnOrderings(column), @@ -15164,6 +15356,11 @@ class $$MediaFilesTableOrderingComposer column: $table.createdAt, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get createdAtMonth => $composableBuilder( + column: $table.createdAtMonth, + builder: (column) => ColumnOrderings(column), + ); } class $$MediaFilesTableAnnotationComposer @@ -15206,6 +15403,16 @@ class $$MediaFilesTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => column, + ); + + GeneratedColumn get hasCropAnalyzed => $composableBuilder( + column: $table.hasCropAnalyzed, + builder: (column) => column, + ); + GeneratedColumn get preProgressingProcess => $composableBuilder( column: $table.preProgressingProcess, builder: (column) => column, @@ -15255,6 +15462,11 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get createdAtMonth => $composableBuilder( + column: $table.createdAtMonth, + builder: (column) => column, + ); + Expression messagesRefs( Expression Function($$MessagesTableAnnotationComposer a) f, ) { @@ -15316,6 +15528,8 @@ class $$MediaFilesTableTableManager Value requiresAuthentication = const Value.absent(), Value stored = const Value.absent(), Value isDraftMedia = const Value.absent(), + Value isFavorite = const Value.absent(), + Value hasCropAnalyzed = const Value.absent(), Value preProgressingProcess = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), @@ -15326,6 +15540,7 @@ class $$MediaFilesTableTableManager Value encryptionNonce = const Value.absent(), Value storedFileHash = const Value.absent(), Value createdAt = const Value.absent(), + Value createdAtMonth = const Value.absent(), Value rowid = const Value.absent(), }) => MediaFilesCompanion( mediaId: mediaId, @@ -15335,6 +15550,8 @@ class $$MediaFilesTableTableManager requiresAuthentication: requiresAuthentication, stored: stored, isDraftMedia: isDraftMedia, + isFavorite: isFavorite, + hasCropAnalyzed: hasCropAnalyzed, preProgressingProcess: preProgressingProcess, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, @@ -15345,6 +15562,7 @@ class $$MediaFilesTableTableManager encryptionNonce: encryptionNonce, storedFileHash: storedFileHash, createdAt: createdAt, + createdAtMonth: createdAtMonth, rowid: rowid, ), createCompanionCallback: @@ -15356,6 +15574,8 @@ class $$MediaFilesTableTableManager Value requiresAuthentication = const Value.absent(), Value stored = const Value.absent(), Value isDraftMedia = const Value.absent(), + Value isFavorite = const Value.absent(), + Value hasCropAnalyzed = const Value.absent(), Value preProgressingProcess = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), @@ -15366,6 +15586,7 @@ class $$MediaFilesTableTableManager Value encryptionNonce = const Value.absent(), Value storedFileHash = const Value.absent(), Value createdAt = const Value.absent(), + Value createdAtMonth = const Value.absent(), Value rowid = const Value.absent(), }) => MediaFilesCompanion.insert( mediaId: mediaId, @@ -15375,6 +15596,8 @@ class $$MediaFilesTableTableManager requiresAuthentication: requiresAuthentication, stored: stored, isDraftMedia: isDraftMedia, + isFavorite: isFavorite, + hasCropAnalyzed: hasCropAnalyzed, preProgressingProcess: preProgressingProcess, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, @@ -15385,6 +15608,7 @@ class $$MediaFilesTableTableManager encryptionNonce: encryptionNonce, storedFileHash: storedFileHash, createdAt: createdAt, + createdAtMonth: createdAtMonth, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index 54bf66ee..4c8cd6d5 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -7056,6 +7056,511 @@ i1.GeneratedColumn _column_238(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', ); + +final class Schema14 extends i0.VersionedSchema { + Schema14({required super.database}) : super(version: 14); + @override + late final List entities = [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + late final Shape39 contacts = Shape39( + source: i0.VersionedTable( + entityName: 'contacts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(user_id)'], + columns: [ + _column_106, + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_211, + _column_212, + _column_213, + _column_214, + _column_215, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape23 groups = Shape23( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id)'], + columns: [ + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + _column_130, + _column_131, + _column_132, + _column_133, + _column_134, + _column_118, + _column_135, + _column_136, + _column_137, + _column_138, + _column_139, + _column_140, + _column_141, + _column_142, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 mediaFiles = Shape49( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(media_id)'], + columns: [ + _column_143, + _column_144, + _column_145, + _column_146, + _column_147, + _column_148, + _column_149, + _column_239, + _column_240, + _column_207, + _column_150, + _column_151, + _column_152, + _column_153, + _column_154, + _column_155, + _column_156, + _column_157, + _column_118, + _column_241, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 messages = Shape25( + source: i0.VersionedTable( + entityName: 'messages', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id)'], + columns: [ + _column_158, + _column_159, + _column_160, + _column_144, + _column_161, + _column_162, + _column_163, + _column_164, + _column_165, + _column_153, + _column_166, + _column_167, + _column_168, + _column_169, + _column_118, + _column_170, + _column_171, + _column_172, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 messageHistories = Shape26( + source: i0.VersionedTable( + entityName: 'message_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_173, + _column_174, + _column_175, + _column_161, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 reactions = Shape27( + source: i0.VersionedTable( + entityName: 'reactions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'], + columns: [_column_174, _column_176, _column_177, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 groupMembers = Shape38( + source: i0.VersionedTable( + entityName: 'group_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id, contact_id)'], + columns: [ + _column_158, + _column_178, + _column_179, + _column_180, + _column_209, + _column_210, + _column_181, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape37 receipts = Shape37( + source: i0.VersionedTable( + entityName: 'receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [ + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + _column_208, + _column_187, + _column_188, + _column_189, + _column_190, + _column_191, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape30 receivedReceipts = Shape30( + source: i0.VersionedTable( + entityName: 'received_receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [_column_182, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape31 signalIdentityKeyStores = Shape31( + source: i0.VersionedTable( + entityName: 'signal_identity_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_194, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 signalPreKeyStores = Shape32( + source: i0.VersionedTable( + entityName: 'signal_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(pre_key_id)'], + columns: [_column_195, _column_196, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 signalSenderKeyStores = Shape11( + source: i0.VersionedTable( + entityName: 'signal_sender_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(sender_key_name)'], + columns: [_column_197, _column_198], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape33 signalSessionStores = Shape33( + source: i0.VersionedTable( + entityName: 'signal_session_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_199, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape34 messageActions = Shape34( + source: i0.VersionedTable( + entityName: 'message_actions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'], + columns: [_column_174, _column_183, _column_144, _column_200], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 groupHistories = Shape35( + source: i0.VersionedTable( + entityName: 'group_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_history_id)'], + columns: [ + _column_201, + _column_158, + _column_202, + _column_203, + _column_204, + _column_205, + _column_206, + _column_144, + _column_200, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape40 keyVerifications = Shape40( + source: i0.VersionedTable( + entityName: 'key_verifications', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_216, _column_183, _column_144, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 verificationTokens = Shape41( + source: i0.VersionedTable( + entityName: 'verification_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_217, _column_218, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape42 userDiscoveryAnnouncedUsers = Shape42( + source: i0.VersionedTable( + entityName: 'user_discovery_announced_users', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id)'], + columns: [ + _column_219, + _column_220, + _column_221, + _column_222, + _column_223, + _column_224, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 userDiscoveryUserRelations = Shape43( + source: i0.VersionedTable( + entityName: 'user_discovery_user_relations', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'], + columns: [_column_225, _column_226, _column_227], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 userDiscoveryOtherPromotions = Shape44( + source: i0.VersionedTable( + entityName: 'user_discovery_other_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'], + columns: [ + _column_226, + _column_228, + _column_229, + _column_230, + _column_231, + _column_227, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 userDiscoveryOwnPromotions = Shape45( + source: i0.VersionedTable( + entityName: 'user_discovery_own_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_232, _column_183, _column_233], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 userDiscoveryShares = Shape46( + source: i0.VersionedTable( + entityName: 'user_discovery_shares', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_234, _column_235, _column_175], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 shortcuts = Shape47( + source: i0.VersionedTable( + entityName: 'shortcuts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_173, _column_236, _column_237], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 shortcutMembers = Shape48( + source: i0.VersionedTable( + entityName: 'shortcut_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'], + columns: [_column_238, _column_158], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape49 extends i0.VersionedTable { + Shape49({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get mediaId => + columnsByName['media_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get uploadState => + columnsByName['upload_state']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadState => + columnsByName['download_state']! as i1.GeneratedColumn; + i1.GeneratedColumn get requiresAuthentication => + columnsByName['requires_authentication']! as i1.GeneratedColumn; + i1.GeneratedColumn get stored => + columnsByName['stored']! as i1.GeneratedColumn; + i1.GeneratedColumn get isDraftMedia => + columnsByName['is_draft_media']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get hasCropAnalyzed => + columnsByName['has_crop_analyzed']! as i1.GeneratedColumn; + i1.GeneratedColumn get preProgressingProcess => + columnsByName['pre_progressing_process']! as i1.GeneratedColumn; + i1.GeneratedColumn get reuploadRequestedBy => + columnsByName['reupload_requested_by']! as i1.GeneratedColumn; + i1.GeneratedColumn get displayLimitInMilliseconds => + columnsByName['display_limit_in_milliseconds']! + as i1.GeneratedColumn; + i1.GeneratedColumn get removeAudio => + columnsByName['remove_audio']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadToken => + columnsByName['download_token']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionKey => + columnsByName['encryption_key']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionMac => + columnsByName['encryption_mac']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionNonce => + columnsByName['encryption_nonce']! as i1.GeneratedColumn; + i1.GeneratedColumn get storedFileHash => + columnsByName['stored_file_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAtMonth => + columnsByName['created_at_month']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_239(String aliasedName) => + i1.GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const i1.CustomExpression('0'), + ); +i1.GeneratedColumn _column_240(String aliasedName) => + i1.GeneratedColumn( + 'has_crop_analyzed', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1))', + defaultValue: const i1.CustomExpression('0'), + ); +i1.GeneratedColumn _column_241(String aliasedName) => + i1.GeneratedColumn( + 'created_at_month', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: 'NULL', + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -7069,6 +7574,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, + required Future Function(i1.Migrator m, Schema14 schema) from13To14, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -7132,6 +7638,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from12To13(migrator, schema); return 13; + case 13: + final schema = Schema14(database: database); + final migrator = i1.Migrator(database, schema); + await from13To14(migrator, schema); + return 14; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -7151,6 +7662,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, + required Future Function(i1.Migrator m, Schema14 schema) from13To14, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -7165,5 +7677,6 @@ i1.OnUpgrade stepByStep({ from10To11: from10To11, from11To12: from11To12, from12To13: from12To13, + from13To14: from13To14, ), ); diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index aa0a86f0..fb92d6a4 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1178,12 +1178,6 @@ abstract class AppLocalizations { /// **'The plan upgrade must be paid for annually, as the current plan is also billed annually.'** String get errorPlanUpgradeNotYearly; - /// No description provided for @upgradeToPaidPlan. - /// - /// In en, this message translates to: - /// **'Upgrade to a paid plan.'** - String get upgradeToPaidPlan; - /// No description provided for @upgradeToPaidPlanButton. /// /// In en, this message translates to: @@ -1322,12 +1316,6 @@ abstract class AppLocalizations { /// **'Delete file'** String get galleryDelete; - /// No description provided for @galleryDetails. - /// - /// In en, this message translates to: - /// **'Show details'** - String get galleryDetails; - /// No description provided for @galleryExport. /// /// In en, this message translates to: @@ -1340,6 +1328,36 @@ abstract class AppLocalizations { /// **'Successfully saved in the Gallery.'** String get galleryExportSuccess; + /// No description provided for @gallerySelectAll. + /// + /// In en, this message translates to: + /// **'Select all'** + String get gallerySelectAll; + + /// No description provided for @galleryDeselectAll. + /// + /// In en, this message translates to: + /// **'Deselect all'** + String get galleryDeselectAll; + + /// No description provided for @galleryFavorite. + /// + /// In en, this message translates to: + /// **'Favorite'** + String get galleryFavorite; + + /// No description provided for @galleryUnfavorite. + /// + /// In en, this message translates to: + /// **'Unfavorite'** + String get galleryUnfavorite; + + /// No description provided for @galleryCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get galleryCancel; + /// No description provided for @memoriesEmpty. /// /// In en, this message translates to: @@ -3157,6 +3175,48 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Emoji already used or invalid'** String get errorEmojiUsedOrInvalid; + + /// No description provided for @subscriptionPledgeTitle. + /// + /// In en, this message translates to: + /// **'Support independent privacy.'** + String get subscriptionPledgeTitle; + + /// No description provided for @subscriptionPledgeSecureTitle. + /// + /// In en, this message translates to: + /// **'Secure by Design'** + String get subscriptionPledgeSecureTitle; + + /// No description provided for @subscriptionPledgeSecureDesc. + /// + /// In en, this message translates to: + /// **'Your messages and shared moments are fully end-to-end encrypted.'** + String get subscriptionPledgeSecureDesc; + + /// No description provided for @subscriptionPledgeNoAdsTitle. + /// + /// In en, this message translates to: + /// **'No Ads or Data selling'** + String get subscriptionPledgeNoAdsTitle; + + /// No description provided for @subscriptionPledgeNoAdsDesc. + /// + /// In en, this message translates to: + /// **'twonly will never show advertisements or sell your private data.'** + String get subscriptionPledgeNoAdsDesc; + + /// No description provided for @subscriptionPledgeFundedTitle. + /// + /// In en, this message translates to: + /// **'Independent and funded by Users'** + String get subscriptionPledgeFundedTitle; + + /// No description provided for @subscriptionPledgeFundedDesc. + /// + /// In en, this message translates to: + /// **'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'** + String get subscriptionPledgeFundedDesc; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2f155db8..6f01aea3 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -601,9 +601,6 @@ class AppLocalizationsDe extends AppLocalizations { String get errorPlanUpgradeNotYearly => 'Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.'; - @override - String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.'; - @override String upgradeToPaidPlanButton(Object planId, Object sufix) { return 'Auf $planId upgraden$sufix'; @@ -677,15 +674,27 @@ class AppLocalizationsDe extends AppLocalizations { @override String get galleryDelete => 'Datei löschen'; - @override - String get galleryDetails => 'Details anzeigen'; - @override String get galleryExport => 'In Galerie exportieren'; @override String get galleryExportSuccess => 'Erfolgreich in der Gallery gespeichert.'; + @override + String get gallerySelectAll => 'Alle auswählen'; + + @override + String get galleryDeselectAll => 'Auswahl aufheben'; + + @override + String get galleryFavorite => 'Als Favorit markieren'; + + @override + String get galleryUnfavorite => 'Favorit entfernen'; + + @override + String get galleryCancel => 'Abbrechen'; + @override String get memoriesEmpty => 'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.'; @@ -1780,4 +1789,29 @@ class AppLocalizationsDe extends AppLocalizations { @override String get errorEmojiUsedOrInvalid => 'Emoji wird bereits verwendet oder ist ungültig'; + + @override + String get subscriptionPledgeTitle => 'Unterstütze unabhängigen Datenschutz.'; + + @override + String get subscriptionPledgeSecureTitle => 'Secure by Design'; + + @override + String get subscriptionPledgeSecureDesc => + 'Deine Nachrichten und Bilder sind immer vollständig Ende-zu-Ende verschlüsselt.'; + + @override + String get subscriptionPledgeNoAdsTitle => 'Keine Werbung oder Datenverkauf'; + + @override + String get subscriptionPledgeNoAdsDesc => + 'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.'; + + @override + String get subscriptionPledgeFundedTitle => + 'Unabhängig und durch Nutzer finanziert'; + + @override + String get subscriptionPledgeFundedDesc => + 'twonly wird rein durch Nutzer-Abonnements finanziert, um unsere Unabhängigkeit und die Zukunft von twonly zu sichern.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 330e9c77..920184bf 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -595,9 +595,6 @@ class AppLocalizationsEn extends AppLocalizations { String get errorPlanUpgradeNotYearly => 'The plan upgrade must be paid for annually, as the current plan is also billed annually.'; - @override - String get upgradeToPaidPlan => 'Upgrade to a paid plan.'; - @override String upgradeToPaidPlanButton(Object planId, Object sufix) { return 'Upgrade to $planId$sufix'; @@ -671,15 +668,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get galleryDelete => 'Delete file'; - @override - String get galleryDetails => 'Show details'; - @override String get galleryExport => 'Export to gallery'; @override String get galleryExportSuccess => 'Successfully saved in the Gallery.'; + @override + String get gallerySelectAll => 'Select all'; + + @override + String get galleryDeselectAll => 'Deselect all'; + + @override + String get galleryFavorite => 'Favorite'; + + @override + String get galleryUnfavorite => 'Unfavorite'; + + @override + String get galleryCancel => 'Cancel'; + @override String get memoriesEmpty => 'As soon as you save pictures or videos, they end up here in your memories.'; @@ -1764,4 +1773,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid'; + + @override + String get subscriptionPledgeTitle => 'Support independent privacy.'; + + @override + String get subscriptionPledgeSecureTitle => 'Secure by Design'; + + @override + String get subscriptionPledgeSecureDesc => + 'Your messages and shared moments are fully end-to-end encrypted.'; + + @override + String get subscriptionPledgeNoAdsTitle => 'No Ads or Data selling'; + + @override + String get subscriptionPledgeNoAdsDesc => + 'twonly will never show advertisements or sell your private data.'; + + @override + String get subscriptionPledgeFundedTitle => 'Independent and funded by Users'; + + @override + String get subscriptionPledgeFundedDesc => + 'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 9218abf0..10b4bfed 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 9218abf0961c072edd2f8aa5035d06a331b853c6 +Subproject commit 10b4bfedcc6c99e4ba0374b306610d297b9e2276 diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index f0a95327..08bda092 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -5,9 +5,11 @@ class MemoryItem { MemoryItem({ required this.mediaService, required this.messages, + this.sender, }); final List messages; final MediaFileService mediaService; + final Contact? sender; static Future> convertFromMessages( List messages, diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index edd82990..fc1d4784 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:image/image.dart' as img; import 'package:path/path.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; @@ -372,4 +374,142 @@ class MediaFileService { namePrefix: '.overlay', extensionParam: 'png', ); + + Future cropTransparentBorders() async { + if (mediaFile.type != MediaType.image) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion(hasCropAnalyzed: Value(true)), + ); + return; + } + + if (!storedPath.existsSync()) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion(hasCropAnalyzed: Value(true)), + ); + return; + } + + try { + final bytes = storedPath.readAsBytesSync(); + final image = img.decodeImage(bytes); + if (image == null) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion(hasCropAnalyzed: Value(true)), + ); + return; + } + + var minY = 0; + var maxY = image.height - 1; + var minX = 0; + var maxX = image.width - 1; + + var found = false; + for (var y = 0; y < image.height; y++) { + for (var x = 0; x < image.width; x++) { + if (image.getPixel(x, y).a > 10) { + minY = y; + found = true; + break; + } + } + if (found) break; + } + + found = false; + for (var y = image.height - 1; y >= minY; y--) { + for (var x = 0; x < image.width; x++) { + if (image.getPixel(x, y).a > 10) { + maxY = y; + found = true; + break; + } + } + if (found) break; + } + + found = false; + for (var x = 0; x < image.width; x++) { + for (var y = minY; y <= maxY; y++) { + if (image.getPixel(x, y).a > 10) { + minX = x; + found = true; + break; + } + } + if (found) break; + } + + found = false; + for (var x = image.width - 1; x >= minX; x--) { + for (var y = minY; y <= maxY; y++) { + if (image.getPixel(x, y).a > 10) { + maxX = x; + found = true; + break; + } + } + if (found) break; + } + + final newWidth = maxX - minX + 1; + final newHeight = maxY - minY + 1; + + if (minY > 0 || + maxY < image.height - 1 || + minX > 0 || + maxX < image.width - 1) { + if (newWidth > 10 && newHeight > 10) { + final cropped = img.copyCrop( + image, + x: minX, + y: minY, + width: newWidth, + height: newHeight, + ); + final pngBytes = img.encodePng(cropped); + final webpBytes = await FlutterImageCompress.compressWithList( + pngBytes, + format: CompressFormat.webp, + quality: 90, + ); + storedPath.writeAsBytesSync(webpBytes); + + if (thumbnailPath.existsSync()) { + thumbnailPath.deleteSync(); + } + await createThumbnail(); + final checksum = await sha256File(storedPath); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + hasCropAnalyzed: const Value(true), + storedFileHash: Value(Uint8List.fromList(checksum)), + ), + ); + await updateFromDB(); + return; + } + } + + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion(hasCropAnalyzed: Value(true)), + ); + await updateFromDB(); + } catch (e) { + Log.error( + 'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e', + ); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion(hasCropAnalyzed: Value(true)), + ); + await updateFromDB(); + } + } } diff --git a/lib/src/services/memories/memories.service.dart b/lib/src/services/memories/memories.service.dart new file mode 100644 index 00000000..4c275f43 --- /dev/null +++ b/lib/src/services/memories/memories.service.dart @@ -0,0 +1,332 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:clock/clock.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/log.dart'; + +class MemoriesState { + const MemoriesState({ + required this.filesToMigrate, + required this.galleryItems, + required this.months, + required this.orderedByMonth, + required this.galleryItemsLastYears, + }); + + final int filesToMigrate; + final List galleryItems; + final List months; + final Map> orderedByMonth; + final Map> galleryItemsLastYears; + + bool get isLoading => filesToMigrate > 0; + bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0; + + MemoriesState copyWith({ + int? filesToMigrate, + List? galleryItems, + List? months, + Map>? orderedByMonth, + Map>? galleryItemsLastYears, + }) { + return MemoriesState( + filesToMigrate: filesToMigrate ?? this.filesToMigrate, + galleryItems: galleryItems ?? this.galleryItems, + months: months ?? this.months, + orderedByMonth: orderedByMonth ?? this.orderedByMonth, + galleryItemsLastYears: + galleryItemsLastYears ?? this.galleryItemsLastYears, + ); + } +} + +class MemoriesService { + MemoriesService() { + if (_cachedState != null) { + _currentState = _cachedState!; + } + unawaited(_initAsync()); + } + + static MemoriesState? _cachedState; + + final _stateController = StreamController.broadcast(); + Stream get watchState => _stateController.stream; + + MemoriesState _currentState = const MemoriesState( + filesToMigrate: 0, + galleryItems: [], + months: [], + orderedByMonth: {}, + galleryItemsLastYears: {}, + ); + + MemoriesState get currentState => _currentState; + + StreamSubscription>? _dbSubscription; + + /// Instantly pre-warms the gallery state from disk cache during app loading + static Future prewarmCache() async { + try { + final data = await KeyValueStore.get('memories_cache'); + if (data != null && data['items'] is List) { + final itemList = data['items'] as List; + if (itemList.isEmpty) return; + + final mediaIds = itemList + .map((e) => (e as Map)['mediaId'] as String?) + .whereType() + .toList(); + + final mediaFiles = + await twonlyDB.mediaFilesDao.getMediaFilesByIds(mediaIds); + final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m}; + + final allContacts = await twonlyDB.contactsDao.getAllContacts(); + final contactMap = {for (final c in allContacts) c.userId: c}; + + final now = clock.now(); + final tempGalleryItems = []; + final tempGalleryItemsLastYears = >{}; + + for (final itemJson in itemList) { + final map = itemJson as Map; + final mediaId = map['mediaId'] as String?; + final senderUserId = map['senderUserId'] as int?; + if (mediaId == null) continue; + + final mediaFile = mediaFileMap[mediaId]; + if (mediaFile == null) continue; + + final mediaService = MediaFileService(mediaFile); + if (!mediaService.imagePreviewAvailable) continue; + + final contact = + senderUserId != null ? contactMap[senderUserId] : null; + final item = MemoryItem( + mediaService: mediaService, + messages: [], + sender: contact, + ); + tempGalleryItems.add(item); + + if (mediaFile.createdAt.month == now.month && + mediaFile.createdAt.day == now.day) { + final diff = now.year - mediaFile.createdAt.year; + if (diff > 0) { + tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item); + } + } + } + + final tempOrderedByMonth = >{}; + final tempMonths = []; + var lastMonth = ''; + + for (var i = 0; i < tempGalleryItems.length; i++) { + final mFile = tempGalleryItems[i].mediaService.mediaFile; + final month = mFile.createdAtMonth ?? + DateFormat('MMMM yyyy').format(mFile.createdAt); + if (lastMonth != month) { + lastMonth = month; + tempMonths.add(month); + } + tempOrderedByMonth.putIfAbsent(month, () => []).add(i); + } + + for (final list in tempGalleryItemsLastYears.values) { + list.sort( + (a, b) => b.mediaService.mediaFile.createdAt + .compareTo(a.mediaService.mediaFile.createdAt), + ); + } + + final sortedGalleryItemsLastYears = + SplayTreeMap>.from(tempGalleryItemsLastYears); + + _cachedState = MemoriesState( + filesToMigrate: 0, + galleryItems: tempGalleryItems, + months: tempMonths, + orderedByMonth: tempOrderedByMonth, + galleryItemsLastYears: sortedGalleryItemsLastYears, + ); + } + } catch (e) { + Log.error('Error prewarming memories cache: $e'); + } + } + + Future _initAsync() async { + try { + // 1. Perform Inventory / Migration of non-hashed stored files + final nonHashedFiles = + await twonlyDB.mediaFilesDao.getAllNonHashedStoredMediaFiles(); + final unanalyzedFiles = + await twonlyDB.mediaFilesDao.getAllUnanalyzedStoredMediaFiles(); + + final totalToMigrate = nonHashedFiles.length + unanalyzedFiles.length; + if (totalToMigrate > 0) { + _updateState(filesToMigrate: totalToMigrate); + + for (final mediaFile in nonHashedFiles) { + final mediaService = MediaFileService(mediaFile); + await mediaService.hashStoredMedia(); + _updateState(filesToMigrate: _currentState.filesToMigrate - 1); + } + + for (final mediaFile in unanalyzedFiles) { + final mediaService = MediaFileService(mediaFile); + await mediaService.cropTransparentBorders(); + _updateState(filesToMigrate: _currentState.filesToMigrate - 1); + } + + _updateState(filesToMigrate: 0); + } + + // 2. Subscribe to stored media files stream + await _dbSubscription?.cancel(); + _dbSubscription = twonlyDB.mediaFilesDao + .watchAllStoredMediaFiles() + .listen(_processMediaFilesStream); + } catch (e) { + Log.error('Error initializing MemoriesService: $e'); + } + } + + Future _processMediaFilesStream(List mediaFiles) async { + try { + final now = clock.now(); + final tempGalleryItems = []; + final tempGalleryItemsLastYears = >{}; + + // High-performance batch DB fetch for sender attribution via Messages table mapping + final mediaIds = mediaFiles.map((m) => m.mediaId).toList(); + final allMessages = + await twonlyDB.messagesDao.getMessagesByMediaIds(mediaIds); + final allContacts = await twonlyDB.contactsDao.getAllContacts(); + + final contactMap = {for (final c in allContacts) c.userId: c}; + final mediaIdToSenderContact = {}; + + for (final msg in allMessages) { + if (msg.mediaId != null && msg.senderId != null) { + final contact = contactMap[msg.senderId]; + if (contact != null) { + mediaIdToSenderContact[msg.mediaId!] = contact; + } + } + } + + for (final mediaFile in mediaFiles) { + final mediaService = MediaFileService(mediaFile); + if (!mediaService.imagePreviewAvailable) continue; + + if (mediaService.mediaFile.type == MediaType.video) { + if (!mediaService.thumbnailPath.existsSync()) { + unawaited(mediaService.createThumbnail()); + } + } + + final senderContact = mediaIdToSenderContact[mediaFile.mediaId]; + final item = MemoryItem( + mediaService: mediaService, + messages: [], + sender: senderContact, + ); + + tempGalleryItems.add(item); + + if (mediaFile.createdAt.month == now.month && + mediaFile.createdAt.day == now.day) { + final diff = now.year - mediaFile.createdAt.year; + if (diff > 0) { + tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item); + } + } + } + + // Sort descending by creation date + tempGalleryItems.sort( + (a, b) => b.mediaService.mediaFile.createdAt + .compareTo(a.mediaService.mediaFile.createdAt), + ); + + final tempOrderedByMonth = >{}; + final tempMonths = []; + var lastMonth = ''; + + // High performance grouping leveraging pre-computed createdAtMonth column + for (var i = 0; i < tempGalleryItems.length; i++) { + final mFile = tempGalleryItems[i].mediaService.mediaFile; + final month = mFile.createdAtMonth ?? + DateFormat('MMMM yyyy').format(mFile.createdAt); + if (lastMonth != month) { + lastMonth = month; + tempMonths.add(month); + } + tempOrderedByMonth.putIfAbsent(month, () => []).add(i); + } + + for (final list in tempGalleryItemsLastYears.values) { + list.sort( + (a, b) => b.mediaService.mediaFile.createdAt + .compareTo(a.mediaService.mediaFile.createdAt), + ); + } + + final sortedGalleryItemsLastYears = + SplayTreeMap>.from(tempGalleryItemsLastYears); + + final newState = MemoriesState( + filesToMigrate: _currentState.filesToMigrate, + galleryItems: tempGalleryItems, + months: tempMonths, + orderedByMonth: tempOrderedByMonth, + galleryItemsLastYears: sortedGalleryItemsLastYears, + ); + + _cachedState = newState; + _updateStateWithObject(newState); + + // Persist to KeyValueStore cache asynchronously + final cacheList = tempGalleryItems + .map( + (item) => { + 'mediaId': item.mediaService.mediaFile.mediaId, + 'senderUserId': item.sender?.userId, + }, + ) + .toList(); + unawaited(KeyValueStore.put('memories_cache', {'items': cacheList})); + } catch (e) { + Log.error('Error processing media files stream in MemoriesService: $e'); + } + } + + void _updateStateWithObject(MemoriesState newState) { + _currentState = newState; + if (!_stateController.isClosed) { + _stateController.add(_currentState); + } + } + + void _updateState({int? filesToMigrate}) { + _currentState = _currentState.copyWith(filesToMigrate: filesToMigrate); + if (!_stateController.isClosed) { + _stateController.add(_currentState); + } + } + + void dispose() { + _dbSubscription?.cancel(); + _stateController.close(); + } +} diff --git a/lib/src/visual/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/visual/views/chats/chat_messages_components/in_chat_media_viewer.dart index 9a10965c..4681f77c 100644 --- a/lib/src/visual/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/visual/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -6,8 +6,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/message_send_state_icon.dart'; -import 'package:twonly/src/visual/views/shared/memory_item_slider.view.dart'; -import 'package:twonly/src/visual/views/shared/memory_item_thumbnail.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; +import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class InChatMediaViewer extends StatefulWidget { const InChatMediaViewer({ @@ -36,6 +36,8 @@ class _InChatMediaViewerState extends State { int? galleryItemIndex; StreamSubscription? messageStream; Timer? _timer; + late final ValueNotifier _activeMediaIdNotifier = + ValueNotifier(widget.message.mediaId); @override void initState() { @@ -71,10 +73,10 @@ class _InChatMediaViewerState extends State { @override void dispose() { - super.dispose(); messageStream?.cancel(); _timer?.cancel(); - // videoController?.dispose(); + _activeMediaIdNotifier.dispose(); + super.dispose(); } Future initStream() async { @@ -99,14 +101,27 @@ class _InChatMediaViewerState extends State { Future onTap() async { if (galleryItemIndex == null) return; + _activeMediaIdNotifier.value = widget.message.mediaId; + await Navigator.push( context, PageRouteBuilder( opaque: false, - pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( - galleryItems: widget.galleryItems, - initialIndex: galleryItemIndex!, - ), + transitionDuration: const Duration(milliseconds: 350), + reverseTransitionDuration: const Duration(milliseconds: 350), + pageBuilder: (context, animation, secondaryAnimation) { + return SynchronizedImageViewerScreen( + galleryItems: widget.galleryItems, + initialIndex: galleryItemIndex!, + activeMediaIdNotifier: _activeMediaIdNotifier, + ); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, ), ); } @@ -148,9 +163,10 @@ class _InChatMediaViewerState extends State { borderRadius: BorderRadius.circular(12), ), child: galleryItemIndex != null - ? MemoriesItemThumbnailComp( + ? MemoriesThumbnailComp( galleryItem: widget.galleryItems[galleryItemIndex!], onTap: onTap, + activeMediaIdNotifier: _activeMediaIdNotifier, ) : null, ); diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index 6ea2155c..3f9afecf 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -20,7 +20,7 @@ import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; import 'package:twonly/src/visual/views/chats/message_info.view.dart'; -import 'package:twonly/src/visual/views/shared/memory_item_slider.view.dart'; +import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ @@ -77,9 +77,22 @@ class MessageContextMenu extends StatelessWidget { context, PageRouteBuilder( opaque: false, - pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( - galleryItems: galleryItems, - ), + transitionDuration: const Duration(milliseconds: 350), + reverseTransitionDuration: const Duration(milliseconds: 350), + pageBuilder: (context, animation, secondaryAnimation) { + return SynchronizedImageViewerScreen( + galleryItems: galleryItems, + initialIndex: 0, + activeMediaIdNotifier: + ValueNotifier(mediaFileService!.mediaFile.mediaId), + ); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, ), ); } diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index e5afc67b..ae6221fa 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -36,6 +36,7 @@ class HomeViewState extends State { int _activePageIdx = 1; double _offsetRatio = 0; double _offsetFromOne = 0; + bool _isBottomNavVisible = true; Timer? _disableCameraTimer; final MainCameraController _mainCameraController = MainCameraController(); @@ -174,6 +175,29 @@ class HomeViewState extends State { bool _onPageView(ScrollNotification notification) { _disableCameraTimer?.cancel(); + if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) { + if (_activePageIdx == 2 && + notification.metrics.pixels < 100 && + !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } else if (notification is ScrollUpdateNotification) { + final delta = notification.scrollDelta ?? 0; + if (delta > 5 && + _isBottomNavVisible && + (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { + setState(() { + _isBottomNavVisible = false; + }); + } else if (delta < -5 && !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } + } + } + if (notification.depth == 0 && notification is ScrollUpdateNotification) { setState(() { _offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0); @@ -259,39 +283,48 @@ class HomeViewState extends State { ], ), ), - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - unselectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), - ), - selectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface, - ), - items: const [ - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.solidComments), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.camera), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.photoFilm), - label: '', - ), - ], - onTap: (index) async { - _activePageIdx = index; - await _homeViewPageController.animateToPage( - index, - duration: const Duration(milliseconds: 100), - curve: Curves.bounceIn, - ); - if (mounted) setState(() {}); - }, - currentIndex: _activePageIdx, + bottomNavigationBar: AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: _isBottomNavVisible + ? BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + unselectedIconTheme: IconThemeData( + color: Theme.of(context) + .colorScheme + .inverseSurface + .withAlpha(150), + ), + selectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.inverseSurface, + ), + items: const [ + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.solidComments), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.camera), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.photoFilm), + label: '', + ), + ], + onTap: (index) async { + _activePageIdx = index; + await _homeViewPageController.animateToPage( + index, + duration: const Duration(milliseconds: 100), + curve: Curves.bounceIn, + ); + if (mounted) setState(() {}); + }, + currentIndex: _activePageIdx, + ) + : const SizedBox.shrink(), ), ); } diff --git a/lib/src/visual/views/memories/components/flashback_banner.comp.dart b/lib/src/visual/views/memories/components/flashback_banner.comp.dart new file mode 100644 index 00000000..01bbd48a --- /dev/null +++ b/lib/src/visual/views/memories/components/flashback_banner.comp.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class MemoriesFlashbackBannerComp extends StatelessWidget { + const MemoriesFlashbackBannerComp({ + required this.lastYears, + required this.onOpenFlashback, + super.key, + }); + + final Map> lastYears; + final void Function(List items, int index) onOpenFlashback; + + @override + Widget build(BuildContext context) { + if (lastYears.isEmpty) return const SliverToBoxAdapter(child: SizedBox.shrink()); + + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 150, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: lastYears.length, + separatorBuilder: (context, _) => const SizedBox(width: 12), + itemBuilder: (context, idx) { + final entry = lastYears.entries.elementAt(idx); + final years = entry.key; + final items = entry.value; + + var text = context.lang.memoriesAYearAgo; + if (years > 1) { + text = context.lang.memoriesXYearsAgo(years); + } + + return GestureDetector( + onTap: () => onOpenFlashback(items, 0), + child: Container( + width: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + items.first.mediaService.storedPath, + fit: BoxFit.cover, + ), + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.center, + colors: [ + Colors.black.withValues(alpha: 0.7), + Colors.transparent, + ], + ), + ), + ), + ), + Positioned( + bottom: 12, + left: 8, + right: 8, + child: Text( + text, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart new file mode 100644 index 00000000..9991370d --- /dev/null +++ b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart @@ -0,0 +1,302 @@ +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'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/memories/components/memory_transition_painter.dart'; + +class MemoriesThumbnailComp extends StatefulWidget { + const MemoriesThumbnailComp({ + required this.galleryItem, + required this.onTap, + this.index = 0, + this.onLongPress, + this.selectionMode = false, + this.isSelected = false, + this.activeMediaIdNotifier, + super.key, + }); + + final MemoryItem galleryItem; + final int index; + final GestureTapCallback onTap; + final GestureLongPressCallback? onLongPress; + final bool selectionMode; + final bool isSelected; + final ValueNotifier? activeMediaIdNotifier; + + @override + State createState() => _MemoriesThumbnailCompState(); +} + +final Set _alreadyAnimatedIds = {}; + +class _MemoriesThumbnailCompState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _scaleController; + late final Animation _scaleAnimation; + late final Animation _slideAnimation; + + ImageProvider? _imageProvider; + ImageStream? _imageStream; + ImageInfo? _imageInfo; + late final ImageStreamListener _listener; + + @override + void initState() { + super.initState(); + _scaleController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 350), + ); + _scaleAnimation = Tween(begin: 0.94, end: 1).animate( + CurvedAnimation(parent: _scaleController, curve: Curves.easeOutCubic), + ); + _slideAnimation = + Tween( + begin: const Offset(0, 0.125), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _scaleController, curve: Curves.easeOutCubic), + ); + + final mediaId = widget.galleryItem.mediaService.mediaFile.mediaId; + final shouldAnimate = + widget.index < 20 && !_alreadyAnimatedIds.contains(mediaId); + + if (shouldAnimate) { + _alreadyAnimatedIds.add(mediaId); + final delayMs = widget.index * 10; + if (delayMs > 0) { + Future.delayed(Duration(milliseconds: delayMs), () { + if (mounted) { + _scaleController.forward(); + } + }); + } else { + _scaleController.forward(); + } + } else { + _scaleController.value = 1.0; + } + + _listener = ImageStreamListener((info, _) { + if (mounted) { + setState(() { + _imageInfo = info; + }); + } + }); + _resolveImage(); + } + + void _resolveImage() { + final media = widget.galleryItem.mediaService; + final hasThumbnail = media.thumbnailPath.existsSync(); + final hasStored = media.storedPath.existsSync(); + final isImageOrGif = + media.mediaFile.type == MediaType.image || + media.mediaFile.type == MediaType.gif; + + if (hasThumbnail) { + _imageProvider = FileImage(media.thumbnailPath); + } else if (hasStored && isImageOrGif) { + _imageProvider = FileImage(media.storedPath); + } + + if (_imageProvider != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final config = createLocalImageConfiguration(context); + _imageStream = _imageProvider!.resolve(config); + _imageStream!.addListener(_listener); + }); + } + } + + @override + void didUpdateWidget(covariant MemoriesThumbnailComp oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.galleryItem.mediaService.mediaFile.mediaId != + widget.galleryItem.mediaService.mediaFile.mediaId) { + _imageStream?.removeListener(_listener); + _imageInfo = null; + _resolveImage(); + } + } + + @override + void dispose() { + _scaleController.dispose(); + _imageStream?.removeListener(_listener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final media = widget.galleryItem.mediaService; + final isVideo = media.mediaFile.type == MediaType.video; + final cachedInfo = _imageInfo; + final mediaId = media.mediaFile.mediaId; + + Widget buildHero(String tag) { + return Hero( + key: ValueKey(tag), + tag: tag, + transitionOnUserGestures: true, + flightShuttleBuilder: cachedInfo != null + ? ( + flightContext, + animation, + flightDirection, + fromHeroContext, + toHeroContext, + ) { + return TransitionImage( + imageInfo: cachedInfo, + animation: animation, + thumbnailFit: BoxFit.cover, + viewerFit: BoxFit.contain, + ); + } + : null, + child: SlideTransition( + position: _slideAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: FadeTransition( + opacity: _scaleController, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + decoration: BoxDecoration( + color: widget.isSelected + ? context.color.primary + : Colors.transparent, + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + margin: EdgeInsets.all(widget.isSelected ? 4 : 0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + widget.isSelected ? 12 : 0, + ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + if (cachedInfo != null) + RawImage( + image: cachedInfo.image, + fit: BoxFit.cover, + ) + else if (_imageProvider != null) + Image( + image: _imageProvider!, + fit: BoxFit.cover, + gaplessPlayback: true, + ) + else + ColoredBox( + color: Colors.grey.shade200, + child: const Center( + child: FaIcon( + FontAwesomeIcons.image, + color: Colors.black26, + ), + ), + ), + + if (isVideo) + const Positioned.fill( + child: Center( + child: FaIcon( + FontAwesomeIcons.circlePlay, + color: Colors.white, + size: 32, + shadows: [ + Shadow(color: Colors.black54, blurRadius: 6), + ], + ), + ), + ), + + if (widget.selectionMode) + Positioned( + top: 6, + right: 6, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: widget.isSelected + ? context.color.primary + : Colors.black38, + shape: BoxShape.circle, + border: Border.all( + color: + Theme.of(context).brightness == + Brightness.dark + ? Colors.white + : Colors.black, + width: 1.5, + ), + ), + child: widget.isSelected + ? const Icon( + Icons.check, + size: 14, + color: Colors.white, + ) + : const SizedBox(width: 14, height: 14), + ), + ), + + if (media.mediaFile.isFavorite) + const Positioned( + bottom: 6, + left: 6, + child: Icon( + Icons.favorite, + color: Colors.redAccent, + size: 16, + shadows: [ + Shadow(color: Colors.black54, blurRadius: 4), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: widget.onTap, + onLongPress: widget.onLongPress, + child: widget.activeMediaIdNotifier != null + ? ValueListenableBuilder( + valueListenable: widget.activeMediaIdNotifier!, + builder: (context, activeId, _) { + final isActive = activeId == null || activeId == mediaId; + return buildHero( + isActive ? mediaId : '${mediaId}_grid_inactive', + ); + }, + ) + : buildHero(mediaId), + ); + } +} diff --git a/lib/src/visual/views/memories/components/memory_transition_painter.dart b/lib/src/visual/views/memories/components/memory_transition_painter.dart new file mode 100644 index 00000000..8936755e --- /dev/null +++ b/lib/src/visual/views/memories/components/memory_transition_painter.dart @@ -0,0 +1,104 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; + +class TransitionImage extends StatelessWidget { + const TransitionImage({ + required this.imageInfo, + required this.animation, + required this.thumbnailFit, + required this.viewerFit, + this.background, + super.key, + }); + + final ImageInfo imageInfo; + final Animation animation; + final BoxFit thumbnailFit; + final BoxFit viewerFit; + final Color? background; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) => CustomPaint( + painter: _TransitionImagePainter( + image: imageInfo.image, + scale: imageInfo.scale, + t: animation.value, + thumbnailFit: thumbnailFit, + viewerFit: viewerFit, + background: background, + ), + ), + ); + } +} + +class _TransitionImagePainter extends CustomPainter { + const _TransitionImagePainter({ + required this.image, + required this.scale, + required this.t, + required this.thumbnailFit, + required this.viewerFit, + required this.background, + }); + + final ui.Image? image; + final double scale; + final double t; + final Color? background; + final BoxFit thumbnailFit; + final BoxFit viewerFit; + + static final _paint = Paint() + ..isAntiAlias = true + ..filterQuality = FilterQuality.medium; + static const Alignment _alignment = Alignment.center; + + @override + void paint(Canvas canvas, Size size) { + final targetImage = image; + if (targetImage == null || size.isEmpty) return; + + final outputSize = size; + final inputSize = Size( + targetImage.width.toDouble(), + targetImage.height.toDouble(), + ); + + // Calculate source/destination dimensions for both start and end layout states + final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size); + final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size); + + // Linearly interpolate intermediate source framing and canvas bounds simultaneously + final sourceSize = + Size.lerp(thumbnailSizes.source, viewerSizes.source, t)! * scale; + final destinationSize = Size.lerp( + thumbnailSizes.destination, + viewerSizes.destination, + t, + )!; + + final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; + final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; + final dx = halfWidthDelta + _alignment.x * halfWidthDelta; + final dy = halfHeightDelta + _alignment.y * halfHeightDelta; + final destinationRect = Offset(dx, dy) & destinationSize; + + final sourceRect = _alignment.inscribe(sourceSize, Offset.zero & inputSize); + + if (background != null) { + canvas.drawRect(destinationRect.deflate(1), Paint()..color = background!); + } + canvas.drawImageRect(targetImage, sourceRect, destinationRect, _paint); + } + + @override + bool shouldRepaint(covariant _TransitionImagePainter oldDelegate) { + return oldDelegate.t != t || + oldDelegate.image != image || + oldDelegate.scale != scale; + } +} diff --git a/lib/src/visual/views/memories/components/selection_toolbar.comp.dart b/lib/src/visual/views/memories/components/selection_toolbar.comp.dart new file mode 100644 index 00000000..daf3b5a2 --- /dev/null +++ b/lib/src/visual/views/memories/components/selection_toolbar.comp.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class MemoriesSelectionToolbarComp extends StatelessWidget { + const MemoriesSelectionToolbarComp({ + required this.selectedCount, + required this.areAllSelected, + required this.areAllFav, + required this.onSelectAll, + required this.onExport, + required this.onFavorite, + required this.onDelete, + required this.onClear, + super.key, + }); + + final int selectedCount; + final bool areAllSelected; + final bool areAllFav; + final VoidCallback onSelectAll; + final VoidCallback onExport; + final VoidCallback onFavorite; + final VoidCallback onDelete; + final VoidCallback onClear; + + @override + Widget build(BuildContext context) { + return Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 24, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: context.color.surface.withValues(alpha: 0.95), + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 16, + offset: Offset(0, 4), + ), + ], + border: Border.all( + color: context.color.primary.withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + Text( + '$selectedCount', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const Spacer(), + IconButton( + icon: Icon( + areAllSelected ? Icons.deselect : Icons.select_all, + size: 22, + ), + onPressed: onSelectAll, + tooltip: areAllSelected + ? context.lang.galleryDeselectAll + : context.lang.gallerySelectAll, + style: IconButton.styleFrom( + backgroundColor: context.color.primary.withValues( + alpha: 0.1, + ), + foregroundColor: context.color.primary, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.file_download_outlined, size: 22), + onPressed: onExport, + tooltip: context.lang.galleryExport, + style: IconButton.styleFrom( + backgroundColor: context.color.primary.withValues( + alpha: 0.1, + ), + foregroundColor: context.color.primary, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: Icon( + areAllFav ? Icons.favorite : Icons.favorite_border, + size: 22, + color: areAllFav ? Colors.redAccent : null, + ), + onPressed: onFavorite, + tooltip: areAllFav + ? context.lang.galleryUnfavorite + : context.lang.galleryFavorite, + style: IconButton.styleFrom( + backgroundColor: context.color.primary.withValues( + alpha: 0.1, + ), + foregroundColor: context.color.primary, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.delete_outline, size: 22), + onPressed: onDelete, + tooltip: context.lang.galleryDelete, + style: IconButton.styleFrom( + backgroundColor: Colors.redAccent.withValues( + alpha: 0.1, + ), + foregroundColor: Colors.redAccent, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: onClear, + tooltip: context.lang.galleryCancel, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart b/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart new file mode 100644 index 00000000..717067c3 --- /dev/null +++ b/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class SynchronizedViewerActionsToolbarComp extends StatelessWidget { + const SynchronizedViewerActionsToolbarComp({ + required this.isFavorite, + required this.onShare, + required this.onExport, + required this.onToggleFavorite, + required this.onDelete, + this.showStoreButton = false, + this.onStore, + this.isImageSaving = false, + super.key, + }); + + final bool isFavorite; + final VoidCallback onShare; + final VoidCallback onExport; + final VoidCallback onToggleFavorite; + final VoidCallback onDelete; + final bool showStoreButton; + final VoidCallback? onStore; + final bool isImageSaving; + + @override + Widget build(BuildContext context) { + return Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 24, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showStoreButton) ...[ + IconButton( + icon: isImageSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const FaIcon( + FontAwesomeIcons.floppyDisk, + color: Colors.white, + size: 20, + ), + onPressed: isImageSaving ? null : onStore, + tooltip: 'Store media', + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + padding: const EdgeInsets.all(12), + ), + ), + const SizedBox(width: 16), + ], + IconButton( + icon: const FaIcon( + FontAwesomeIcons.fileArrowDown, + color: Colors.white, + size: 21, + ), + onPressed: onExport, + tooltip: context.lang.galleryExport, + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + padding: const EdgeInsets.all(12), + ), + ), + const SizedBox(width: 16), + IconButton( + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.redAccent : Colors.white, + size: 24, + ), + onPressed: onToggleFavorite, + tooltip: 'Favorite', + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + padding: const EdgeInsets.all(12), + ), + ), + const SizedBox(width: 16), + IconButton( + icon: const Icon( + Icons.delete_outline, + color: Colors.white, + size: 24, + ), + onPressed: onDelete, + tooltip: context.lang.galleryDelete, + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + padding: const EdgeInsets.all(12), + ), + ), + const SizedBox(width: 16), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + color: primaryColor, + size: 22, + ), + onPressed: onShare, + tooltip: context.lang.shareImagedEditorSendImage, + style: IconButton.styleFrom( + backgroundColor: Colors.black54, + padding: const EdgeInsets.all(12), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index d67f3efa..0a0655a8 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -1,19 +1,18 @@ -// ignore_for_file: parameter_assignments - -import 'dart:async'; - -import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/memories/memories.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; -import 'package:twonly/src/visual/views/shared/memory_item_slider.view.dart'; -import 'package:twonly/src/visual/views/shared/memory_item_thumbnail.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart'; +import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class MemoriesView extends StatefulWidget { const MemoriesView({super.key}); @@ -23,254 +22,498 @@ class MemoriesView extends StatefulWidget { } class MemoriesViewState extends State { - int _filesToMigrate = 0; - List galleryItems = []; - Map> orderedByMonth = {}; - List months = []; - StreamSubscription>? messageSub; + late final MemoriesService _service; + final ValueNotifier _activeMediaIdNotifier = ValueNotifier(null); + final ScrollController _scrollController = ScrollController(); + bool _isViewingFlashback = false; - final Map> _galleryItemsLastYears = {}; + final Set _selectedMediaIds = {}; + bool _filterFavoritesOnly = false; + bool get _selectionMode => _selectedMediaIds.isNotEmpty; @override void initState() { super.initState(); - unawaited(initAsync()); + _service = MemoriesService(); + _activeMediaIdNotifier.addListener(_onActiveMediaChanged); } @override void dispose() { - messageSub?.cancel(); + _activeMediaIdNotifier.removeListener(_onActiveMediaChanged); + _scrollController.dispose(); + _service.dispose(); + _activeMediaIdNotifier.dispose(); super.dispose(); } - Future initAsync() async { - final nonHashedFiles = await twonlyDB.mediaFilesDao - .getAllNonHashedStoredMediaFiles(); - if (nonHashedFiles.isNotEmpty) { - setState(() { - _filesToMigrate = nonHashedFiles.length; - }); - for (final mediaFile in nonHashedFiles) { - final mediaService = MediaFileService(mediaFile); - await mediaService.hashStoredMedia(); - setState(() { - _filesToMigrate -= 1; - }); - } - _filesToMigrate = 0; + void _onActiveMediaChanged() { + if (_isViewingFlashback) return; + final mediaId = _activeMediaIdNotifier.value; + if (mediaId == null) return; + final state = _service.currentState; + if (state.isEmpty) return; + + final index = state.galleryItems.indexWhere( + (item) => item.mediaService.mediaFile.mediaId == mediaId, + ); + if (index == -1) return; + + double offset = 56; + if (state.galleryItemsLastYears.isNotEmpty) { + offset += 220; } - await messageSub?.cancel(); - final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles(); - messageSub = msgStream.listen((mediaFiles) async { - // Group items by month - orderedByMonth = {}; - months = []; - var lastMonth = ''; - galleryItems = []; + final screenWidth = MediaQuery.sizeOf(context).width; + final itemWidth = (screenWidth - 8) / 4; + final itemHeight = itemWidth * (16 / 9); + final rowHeight = itemHeight + 2; - final now = clock.now(); + for (final month in state.months) { + final indices = state.orderedByMonth[month]!; + offset += 44; - for (final mediaFile in mediaFiles) { - final mediaService = MediaFileService(mediaFile); - if (!mediaService.imagePreviewAvailable) continue; - if (mediaService.mediaFile.type == MediaType.video) { - if (!mediaService.thumbnailPath.existsSync()) { - await mediaService.createThumbnail(); - } - } - final item = MemoryItem( - mediaService: mediaService, - messages: [], - ); - galleryItems.add(item); - if (mediaFile.createdAt.month == now.month && - mediaFile.createdAt.day == now.day) { - final diff = now.year - mediaFile.createdAt.year; - if (diff > 0) { - if (!_galleryItemsLastYears.containsKey(diff)) { - _galleryItemsLastYears[diff] = []; - } - _galleryItemsLastYears[diff]!.add(item); - } - } + if (indices.contains(index)) { + final localIdx = indices.indexOf(index); + final row = localIdx ~/ 4; + offset += row * rowHeight; + break; + } else { + final totalRows = (indices.length + 3) ~/ 4; + offset += totalRows * rowHeight; } - galleryItems.sort( - (a, b) => b.mediaService.mediaFile.createdAt.compareTo( - a.mediaService.mediaFile.createdAt, - ), + } + + if (_scrollController.hasClients) { + final targetOffset = (offset - 100).clamp( + 0.0, + _scrollController.position.maxScrollExtent, ); - for (var i = 0; i < galleryItems.length; i++) { - final month = DateFormat( - 'MMMM yyyy', - ).format(galleryItems[i].mediaService.mediaFile.createdAt); - if (lastMonth != month) { - lastMonth = month; - months.add(month); - } - orderedByMonth.putIfAbsent(month, () => []).add(i); - } - if (mounted) { - setState(() {}); + _scrollController.jumpTo(targetOffset); + } + } + + Future _openViewer( + List items, + int index, { + bool isFlashback = false, + }) async { + if (isFlashback) { + _isViewingFlashback = true; + } + _activeMediaIdNotifier.value = items[index].mediaService.mediaFile.mediaId; + + await Navigator.push( + context, + PageRouteBuilder( + opaque: false, + transitionDuration: const Duration(milliseconds: 350), + reverseTransitionDuration: const Duration(milliseconds: 350), + pageBuilder: (context, animation, secondaryAnimation) { + return SynchronizedImageViewerScreen( + galleryItems: items, + initialIndex: index, + activeMediaIdNotifier: _activeMediaIdNotifier, + ); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ), + ); + + if (isFlashback) { + _isViewingFlashback = false; + } + } + + void _toggleSelection(String mediaId) { + setState(() { + if (_selectedMediaIds.contains(mediaId)) { + _selectedMediaIds.remove(mediaId); + } else { + _selectedMediaIds.add(mediaId); } }); } - @override - Widget build(BuildContext context) { - Widget child = Center( - child: Text( - context.lang.memoriesEmpty, - textAlign: TextAlign.center, - ), + void _onLongPressItem(String mediaId) { + setState(() { + _selectedMediaIds.add(mediaId); + }); + } + + void _onTapItem(String mediaId, int globalIndex) { + if (_selectionMode) { + _toggleSelection(mediaId); + } else { + final state = _service.currentState; + var targetItems = state.galleryItems; + var targetIndex = globalIndex; + + if (_filterFavoritesOnly) { + targetItems = state.galleryItems + .where((e) => e.mediaService.mediaFile.isFavorite) + .toList(); + targetIndex = targetItems.indexWhere( + (e) => e.mediaService.mediaFile.mediaId == mediaId, + ); + if (targetIndex == -1) targetIndex = 0; + } + + _openViewer(targetItems, targetIndex); + } + } + + void _selectAll() { + setState(() { + final items = _service.currentState.galleryItems; + final targetIds = {}; + + for (final item in items) { + if (_filterFavoritesOnly) { + if (item.mediaService.mediaFile.isFavorite) { + targetIds.add(item.mediaService.mediaFile.mediaId); + } + } else { + targetIds.add(item.mediaService.mediaFile.mediaId); + } + } + + final areAllSelected = targetIds.every(_selectedMediaIds.contains); + + if (areAllSelected) { + _selectedMediaIds.removeAll(targetIds); + } else { + _selectedMediaIds.addAll(targetIds); + } + }); + } + + Future _batchDelete() async { + final count = _selectedMediaIds.length; + final confirmed = await showAlertDialog( + context, + context.lang.deleteImageTitle, + context.lang.deleteImageBody, ); - if (_filesToMigrate > 0) { - child = Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ThreeRotatingDots( - size: 40, - color: context.color.primary, - ), - const SizedBox(height: 10), - Text( - context.lang.migrationOfMemories(_filesToMigrate), - textAlign: TextAlign.center, - ), - ], - ), + + if (!confirmed) return; + + final items = _service.currentState.galleryItems; + for (final mediaId in _selectedMediaIds) { + final item = items + .where((e) => e.mediaService.mediaFile.mediaId == mediaId) + .firstOrNull; + if (item != null) { + item.mediaService.fullMediaRemoval(); + } + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); + } + + setState(_selectedMediaIds.clear); + + if (!mounted) return; + showSnackbar( + context, + 'Deleted $count items successfully', + level: SnackbarLevel.success, + ); + } + + Future _batchExport() async { + final items = _service.currentState.galleryItems; + + try { + for (final mediaId in _selectedMediaIds) { + final item = items + .where((e) => e.mediaService.mediaFile.mediaId == mediaId) + .firstOrNull; + if (item != null) { + final media = item.mediaService; + if (media.mediaFile.type == MediaType.video) { + await saveVideoToGallery(media.storedPath.path); + } else if (media.mediaFile.type == MediaType.image || + media.mediaFile.type == MediaType.gif) { + final imageBytes = await media.storedPath.readAsBytes(); + await saveImageToGallery(imageBytes); + } + } + } + + if (!mounted) return; + showSnackbar( + context, + context.lang.galleryExportSuccess, + level: SnackbarLevel.success, ); - } else if (galleryItems.isNotEmpty) { - child = ListView.builder( - itemCount: - (months.length * 2) + (_galleryItemsLastYears.isEmpty ? 0 : 1), - itemBuilder: (context, mIndex) { - if (_galleryItemsLastYears.isNotEmpty && mIndex == 0) { - return SizedBox( - height: 140, - width: MediaQuery.sizeOf(context).width, - child: ListView( - scrollDirection: Axis.horizontal, - children: _galleryItemsLastYears.entries.map( - (item) { - var text = context.lang.memoriesAYearAgo; - if (item.key > 1) { - text = context.lang.memoriesXYearsAgo(item.key); - } - return GestureDetector( - onTap: () async { - await open(context, item.value, 0); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - spreadRadius: -12, - blurRadius: 12, - ), - ], - ), - clipBehavior: Clip.hardEdge, - height: 150, - width: 120, - child: Stack( - children: [ - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file( - item.value.first.mediaService.storedPath, - fit: BoxFit.cover, - ), - ), - ), - Positioned( - bottom: 10, - left: 0, - right: 0, - child: Text( - text, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 20, - shadows: [ - Shadow( - color: Color.fromARGB(122, 0, 0, 0), - blurRadius: 5, - ), - ], - ), - ), - ), - ], - ), - ), - ); - }, - ).toList(), - ), - ); - } - if (_galleryItemsLastYears.isNotEmpty) { - mIndex -= 1; - } - if (mIndex.isEven) { - return Padding( - padding: const EdgeInsets.all(8), - child: Text(months[(mIndex ~/ 2)]), - ); - } - final index = (mIndex - 1) ~/ 2; - return GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 9 / 16, - ), - itemCount: orderedByMonth[months[index]]!.length, - itemBuilder: (context, gIndex) { - final gaIndex = orderedByMonth[months[index]]![gIndex]; - return MemoriesItemThumbnailComp( - galleryItem: galleryItems[gaIndex], - onTap: () async { - await open(context, galleryItems, gaIndex); - }, - ); - }, - ); - }, + } catch (e) { + if (!mounted) return; + showSnackbar(context, e.toString()); + } + } + + Future _batchFavorite() async { + final items = _service.currentState.galleryItems; + var favCount = 0; + for (final item in items) { + if (_selectedMediaIds.contains(item.mediaService.mediaFile.mediaId)) { + if (item.mediaService.mediaFile.isFavorite) { + favCount++; + } + } + } + final areAllFav = + _selectedMediaIds.isNotEmpty && favCount == _selectedMediaIds.length; + final targetFav = !areAllFav; + + for (final mediaId in _selectedMediaIds) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + MediaFilesCompanion(isFavorite: Value(targetFav)), ); } + setState(() {}); + } + + @override + Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Memories')), - body: Scrollbar( - child: child, + body: Stack( + fit: StackFit.expand, + children: [ + StreamBuilder( + initialData: _service.currentState, + stream: _service.watchState, + builder: (context, snapshot) { + final state = snapshot.data ?? _service.currentState; + + if (state.isLoading) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ThreeRotatingDots( + size: 40, + color: context.color.primary, + ), + const SizedBox(height: 16), + Text( + context.lang.migrationOfMemories(state.filesToMigrate), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + if (state.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.photo_library_outlined, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + context.lang.memoriesEmpty, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + var months = state.months; + var orderedByMonth = state.orderedByMonth; + final lastYears = state.galleryItemsLastYears; + + if (_filterFavoritesOnly) { + final filteredOrdered = >{}; + final filteredMonths = []; + + for (final m in months) { + final indices = orderedByMonth[m] ?? []; + final favIndices = indices.where((idx) { + return state + .galleryItems[idx] + .mediaService + .mediaFile + .isFavorite; + }).toList(); + + if (favIndices.isNotEmpty) { + filteredOrdered[m] = favIndices; + filteredMonths.add(m); + } + } + + months = filteredMonths; + orderedByMonth = filteredOrdered; + } + + return Scrollbar( + controller: _scrollController, + thickness: 12, + radius: const Radius.circular(6), + interactive: true, + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + title: const Text( + 'Memories', + style: TextStyle(fontWeight: FontWeight.bold), + ), + floating: true, + snap: true, + elevation: 0, + backgroundColor: context.color.surface, + actions: [ + IconButton( + icon: Icon( + _filterFavoritesOnly + ? Icons.favorite + : Icons.favorite_border, + color: _filterFavoritesOnly + ? Colors.redAccent + : null, + ), + onPressed: () { + setState(() { + _filterFavoritesOnly = !_filterFavoritesOnly; + }); + }, + tooltip: _filterFavoritesOnly + ? 'Show all' + : 'Show favorites only', + ), + ], + ), + + MemoriesFlashbackBannerComp( + lastYears: lastYears, + onOpenFlashback: (items, idx) => + _openViewer(items, idx, isFlashback: true), + ), + + for (final month in months) ...[ + SliverPadding( + padding: const EdgeInsets.fromLTRB(8, 12, 8, 6), + sliver: SliverToBoxAdapter( + child: Text( + month, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 4), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + childAspectRatio: 9 / 16, + ), + delegate: SliverChildBuilderDelegate( + (context, idx) { + final globalIndex = orderedByMonth[month]![idx]; + final item = state.galleryItems[globalIndex]; + final mediaId = + item.mediaService.mediaFile.mediaId; + final isSelected = _selectedMediaIds.contains( + mediaId, + ); + + return MemoriesThumbnailComp( + galleryItem: item, + index: globalIndex, + selectionMode: _selectionMode, + isSelected: isSelected, + activeMediaIdNotifier: _activeMediaIdNotifier, + onLongPress: () => _onLongPressItem(mediaId), + onTap: () => _onTapItem(mediaId, globalIndex), + ); + }, + childCount: orderedByMonth[month]!.length, + ), + ), + ), + ], + const SliverPadding(padding: EdgeInsets.only(bottom: 32)), + ], + ), + ); + }, + ), + + if (_selectionMode) + Builder( + builder: (context) { + final items = _service.currentState.galleryItems; + var visibleCount = 0; + var favCount = 0; + + for (final item in items) { + final isFav = item.mediaService.mediaFile.isFavorite; + if (!_filterFavoritesOnly || isFav) { + visibleCount++; + } + if (_selectedMediaIds.contains( + item.mediaService.mediaFile.mediaId, + )) { + if (isFav) { + favCount++; + } + } + } + + final areAllSelected = + visibleCount > 0 && + _selectedMediaIds.length >= visibleCount; + final areAllFav = + _selectedMediaIds.isNotEmpty && + favCount == _selectedMediaIds.length; + + return MemoriesSelectionToolbarComp( + selectedCount: _selectedMediaIds.length, + areAllSelected: areAllSelected, + areAllFav: areAllFav, + onSelectAll: _selectAll, + onExport: _batchExport, + onFavorite: _batchFavorite, + onDelete: _batchDelete, + onClear: () => setState(_selectedMediaIds.clear), + ); + }, + ), + ], ), ); } - - Future open( - BuildContext context, - List galleryItems, - int index, - ) async { - await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( - galleryItems: galleryItems, - initialIndex: index, - ), - ), - ) - as bool?; - if (mounted) setState(() {}); - } } diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart new file mode 100644 index 00000000..b7ebc2b8 --- /dev/null +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -0,0 +1,383 @@ +import 'dart:math'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.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.api.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/helpers/video_player_file.helper.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; +import 'package:twonly/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart'; + +class SynchronizedImageViewerScreen extends StatefulWidget { + const SynchronizedImageViewerScreen({ + required this.galleryItems, + required this.initialIndex, + required this.activeMediaIdNotifier, + super.key, + }); + + final List galleryItems; + final int initialIndex; + final ValueNotifier activeMediaIdNotifier; + + @override + State createState() => + _SynchronizedImageViewerScreenState(); +} + +class _SynchronizedImageViewerScreenState + extends State { + late PageController _verticalPager; + late PageController _horizontalPager; + late ValueNotifier _currentlyViewedMediaIdNotifier; + final ValueNotifier _backdropOpacityNotifier = ValueNotifier(1); + + final Set _favoritedMediaIds = {}; + bool _isSaving = false; + final Set _storedMediaIds = {}; + + late int _currentIndex; + bool _isZoomed = false; + + @override + void initState() { + super.initState(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + + _currentIndex = widget.initialIndex; + final initialId = + widget.galleryItems[widget.initialIndex].mediaService.mediaFile.mediaId; + _currentlyViewedMediaIdNotifier = ValueNotifier(initialId); + + _horizontalPager = PageController(initialPage: widget.initialIndex); + _verticalPager = PageController(initialPage: 1); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated); + }); + + for (final item in widget.galleryItems) { + if (item.mediaService.mediaFile.isFavorite) { + _favoritedMediaIds.add(item.mediaService.mediaFile.mediaId); + } + if (item.mediaService.storedPath.existsSync()) { + _storedMediaIds.add(item.mediaService.mediaFile.mediaId); + } + } + } + + Future _storeMediaFile() async { + final item = widget.galleryItems[_currentIndex]; + final mediaId = item.mediaService.mediaFile.mediaId; + setState(() => _isSaving = true); + try { + await item.mediaService.storeMediaFile(); + if (mounted) { + setState(() { + _storedMediaIds.add(mediaId); + }); + } + } finally { + if (mounted) setState(() => _isSaving = false); + } + } + + Future _toggleFavorite(String mediaId) async { + final wasFavorite = _favoritedMediaIds.contains(mediaId); + final isFavoriteNow = !wasFavorite; + + setState(() { + if (isFavoriteNow) { + _favoritedMediaIds.add(mediaId); + } else { + _favoritedMediaIds.remove(mediaId); + } + }); + + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + MediaFilesCompanion(isFavorite: Value(isFavoriteNow)), + ); + } + + void _restoreSystemUI() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + } + + @override + void dispose() { + _restoreSystemUI(); + _verticalPager + ..removeListener(_onVerticalScrollUpdated) + ..dispose(); + _horizontalPager.dispose(); + _currentlyViewedMediaIdNotifier.dispose(); + _backdropOpacityNotifier.dispose(); + super.dispose(); + } + + void _onVerticalScrollUpdated() { + if (!_verticalPager.hasClients) return; + final page = _verticalPager.page ?? 1.0; + + // Map vertical dragging proximity directly to square-root backdrop opacities + final linearFraction = min(1, max(0, page)).toDouble(); + _backdropOpacityNotifier.value = linearFraction * linearFraction; + } + + void _onPageSnapped(int index) { + if (index == 0) { + _triggerSynchronizedPop(); + } + } + + void _triggerSynchronizedPop() { + _restoreSystemUI(); + final targetId = _currentlyViewedMediaIdNotifier.value; + + if (widget.activeMediaIdNotifier.value != targetId) { + widget.activeMediaIdNotifier.value = targetId; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) Navigator.maybeOf(context)?.pop(true); + }); + } else { + Navigator.maybeOf(context)?.pop(true); + } + } + + Future _deleteFile() async { + final confirmed = await showAlertDialog( + context, + context.lang.deleteImageTitle, + context.lang.deleteImageBody, + ); + + if (!confirmed) return; + + widget.galleryItems[_currentIndex].mediaService.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile( + widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId, + ); + + widget.galleryItems.removeAt(_currentIndex); + + if (widget.galleryItems.isEmpty) { + if (mounted) Navigator.pop(context, true); + return; + } + + if (_currentIndex >= widget.galleryItems.length) { + _currentIndex = widget.galleryItems.length - 1; + } + + final newId = + widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId; + _currentlyViewedMediaIdNotifier.value = newId; + widget.activeMediaIdNotifier.value = newId; + + setState(() {}); + } + + Future _exportFile() async { + final item = widget.galleryItems[_currentIndex].mediaService; + + try { + if (item.mediaFile.type == MediaType.video) { + await saveVideoToGallery(item.storedPath.path); + } else if (item.mediaFile.type == MediaType.image || + item.mediaFile.type == MediaType.gif) { + final imageBytes = await item.storedPath.readAsBytes(); + await saveImageToGallery(imageBytes); + } + if (!mounted) return; + showSnackbar( + context, + context.lang.galleryExportSuccess, + level: SnackbarLevel.success, + ); + } catch (e) { + if (!mounted) return; + showSnackbar( + context, + e.toString(), + level: SnackbarLevel.success, + ); + } + } + + Future _shareMediaFile() async { + final orgMediaService = widget.galleryItems[_currentIndex].mediaService; + + final newMediaService = await initializeMediaUpload( + orgMediaService.mediaFile.type, + userService.currentUser.defaultShowTime, + ); + if (newMediaService == null) { + Log.error('Could not create new mediaFile'); + return; + } + + if (orgMediaService.storedPath.existsSync()) { + orgMediaService.storedPath.copySync(newMediaService.originalPath.path); + } else if (orgMediaService.tempPath.existsSync()) { + orgMediaService.tempPath.copySync(newMediaService.originalPath.path); + } + + if (!mounted) return; + + await context.navPush( + ShareImageEditorView( + mediaFileService: newMediaService, + sharedFromGallery: true, + ), + ); + } + + @override + Widget build(BuildContext context) { + if (widget.galleryItems.isEmpty) { + return const SizedBox.shrink(); + } + + final orgMediaService = widget.galleryItems[_currentIndex].mediaService; + final currentMediaId = orgMediaService.mediaFile.mediaId; + + return PopScope( + onPopInvokedWithResult: (didPop, result) { + _restoreSystemUI(); + }, + child: Scaffold( + backgroundColor: Colors.transparent, + body: ValueListenableBuilder( + valueListenable: _backdropOpacityNotifier, + builder: (context, opacity, child) { + return ColoredBox( + color: Colors.black.withValues(alpha: opacity), + child: child, + ); + }, + child: PageView( + controller: _verticalPager, + scrollDirection: Axis.vertical, + physics: _isZoomed + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + onPageChanged: _onPageSnapped, + children: [ + //Fully transparent dismissal trigger anchor + const SizedBox.expand(), + + Stack( + children: [ + PageView.builder( + controller: _horizontalPager, + physics: _isZoomed + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + itemCount: widget.galleryItems.length, + onPageChanged: (idx) { + setState(() { + _currentIndex = idx; + }); + final newMediaId = widget + .galleryItems[idx] + .mediaService + .mediaFile + .mediaId; + _currentlyViewedMediaIdNotifier.value = newMediaId; + widget.activeMediaIdNotifier.value = newMediaId; + }, + itemBuilder: (context, index) { + final item = widget.galleryItems[index]; + final itemMediaId = item.mediaService.mediaFile.mediaId; + + var filePath = item.mediaService.storedPath; + if (!filePath.existsSync()) { + filePath = item.mediaService.tempPath; + } + + final isVideo = + item.mediaService.mediaFile.type == MediaType.video; + + return Center( + child: ValueListenableBuilder( + valueListenable: _currentlyViewedMediaIdNotifier, + builder: (context, activeMediaId, childWidget) { + // Dynamically resolve Hero tags to prevent layout tree duplicate assertions + final isActiveTarget = activeMediaId == itemMediaId; + + if (isActiveTarget) { + return Hero( + tag: itemMediaId, + transitionOnUserGestures: true, + child: childWidget!, + ); + } + return childWidget!; + }, + child: !filePath.existsSync() + ? const Center( + child: Icon( + Icons.broken_image_outlined, + color: Colors.white38, + size: 64, + ), + ) + : isVideo + ? VideoPlayerFileHelper(videoPath: filePath) + : PhotoView( + imageProvider: FileImage(filePath), + initialScale: + PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + maxScale: + PhotoViewComputedScale.covered * 4.1, + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + scaleStateChangedCallback: (state) { + final zoomed = + state != PhotoViewScaleState.initial; + if (_isZoomed != zoomed) { + setState(() { + _isZoomed = zoomed; + }); + } + }, + ), + ), + ); + }, + ), + + SynchronizedViewerActionsToolbarComp( + isFavorite: _favoritedMediaIds.contains(currentMediaId), + onShare: _shareMediaFile, + onExport: _exportFile, + onToggleFavorite: () => _toggleFavorite(currentMediaId), + onDelete: _deleteFile, + showStoreButton: !_storedMediaIds.contains(currentMediaId), + onStore: _storeMediaFile, + isImageSaving: _isSaving, + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/chat/chat_reactions.view.dart b/lib/src/visual/views/settings/chat/chat_reactions.view.dart index 45719eeb..19707b0b 100644 --- a/lib/src/visual/views/settings/chat/chat_reactions.view.dart +++ b/lib/src/visual/views/settings/chat/chat_reactions.view.dart @@ -82,20 +82,6 @@ class _ChatReactionSelectionView extends State { ); }, ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: FloatingActionButton( - foregroundColor: Colors.white, - onPressed: () => UserService.update( - (u) => u.preSelectedEmojies = EmojiAnimationComp - .animatedIcons - .keys - .toList() - .sublist(0, 6), - ), - child: const Icon(Icons.settings_backup_restore_rounded), - ), - ), ); }, ); diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index 8fd830e8..acd73da2 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui' as ui; import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; @@ -6,14 +7,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:restart_app/restart_app.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart'; @@ -25,11 +30,224 @@ class DeveloperSettingsView extends StatefulWidget { } class _DeveloperSettingsViewState extends State { + bool _isGeneratingMockImages = false; + @override void initState() { super.initState(); } + Future _generate1000MockImages() async { + if (_isGeneratingMockImages) return; + setState(() { + _isGeneratingMockImages = true; + }); + + try { + final now = clock.now(); + const groupId = 'mock_group_gallery'; + + // Ensure mock group exists + await twonlyDB.groupsDao.createNewGroup( + const GroupsCompanion( + groupId: Value(groupId), + groupName: Value('Mock Gallery Group'), + isDirectChat: Value(false), + joinedGroup: Value(true), + ), + ); + + const size = Size(360, 640); + + // Batch database entries using cascades for extreme operational speed and clean linting + await twonlyDB.batch((batch) async { + for (var i = 0; i < 1000; i++) { + final mediaId = 'mock_gen_$i'; + final authorIndex = i % 12; + final contactId = 9000000 + authorIndex; + + late DateTime itemDate; + if (i < 200) { + // Spread over the last month + itemDate = now.subtract(Duration(minutes: i * 216)); + } else if (i < 400) { + // Spread between 1 month and 1 year ago + final localI = i - 200; + itemDate = now.subtract( + Duration(days: 30, minutes: localI * 2412), + ); + } else if (i < 600) { + // Around a year ago + final localI = i - 400; + itemDate = now.subtract( + Duration(days: 365, minutes: localI * 216), + ); + } else if (i < 800) { + // Around three years ago + final localI = i - 600; + itemDate = now.subtract( + Duration(days: 1095, minutes: localI * 216), + ); + } else { + // Around four years ago + final localI = i - 800; + itemDate = now.subtract( + Duration(days: 1460, minutes: localI * 216), + ); + } + + batch + ..insert( + twonlyDB.contacts, + ContactsCompanion( + userId: Value(contactId), + username: Value('mock_user_$authorIndex'), + displayName: Value('Author $authorIndex'), + ), + mode: InsertMode.insertOrReplace, + ) + ..insert( + twonlyDB.mediaFiles, + MediaFilesCompanion( + mediaId: Value(mediaId), + type: const Value(MediaType.image), + stored: const Value(true), + createdAt: Value(itemDate), + createdAtMonth: Value(DateFormat('MMMM yyyy').format(itemDate)), + ), + mode: InsertMode.insertOrReplace, + ) + ..insert( + twonlyDB.messages, + MessagesCompanion( + messageId: Value('mock_msg_$i'), + groupId: const Value(groupId), + senderId: Value(contactId), + type: const Value('media'), + mediaId: Value(mediaId), + mediaStored: const Value(true), + openedAt: Value(now), + createdAt: Value(itemDate), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + + // Render custom vector avatars and background colors efficiently + for (var i = 0; i < 1000; i++) { + final mediaId = 'mock_gen_$i'; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + // Background color + final hue = (i * 137.5) % 360; + final bgColor = HSLColor.fromAHSL(1, hue, 0.65, 0.45).toColor(); + canvas.drawRect(Offset.zero & size, Paint()..color = bgColor); + + // Avatar vector representation on it + final center = Offset(size.width / 2, size.height / 2); + final avatarBgPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.25); + canvas.drawCircle(center, 120, avatarBgPaint); + + final eyePaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + final mouthPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 8 + ..strokeCap = StrokeCap.round; + + const eyeOffset = 35.0; + final eyeRadius = 12.0 + (i % 5) * 2; + canvas + ..drawCircle( + center + const Offset(-eyeOffset, -20), + eyeRadius, + eyePaint, + ) + ..drawCircle( + center + const Offset(eyeOffset, -20), + eyeRadius, + eyePaint, + ); + + final mouthRect = Rect.fromCenter( + center: center + const Offset(0, 20), + width: 60, + height: 40, + ); + final startAngle = 0.2 + (i % 3) * 0.1; + final sweepAngle = 2.7 - (i % 3) * 0.2; + canvas.drawArc(mouthRect, startAngle, sweepAngle, false, mouthPaint); + + final textSpan = TextSpan( + text: '#$i', + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: ui.TextDirection.ltr, + )..layout(); + textPainter.paint( + canvas, + Offset((size.width - textPainter.width) / 2, size.height - 80), + ); + + final picture = recorder.endRecording(); + final img = await picture.toImage( + size.width.toInt(), + size.height.toInt(), + ); + final byteData = await img.toByteData(format: ui.ImageByteFormat.png); + if (byteData != null) { + final bytes = byteData.buffer.asUint8List(); + final mediaFile = MediaFile( + mediaId: mediaId, + type: MediaType.image, + stored: true, + requiresAuthentication: false, + isDraftMedia: false, + isFavorite: false, + hasCropAnalyzed: false, + createdAt: now, + ); + final mediaService = MediaFileService(mediaFile); + + if (!mediaService.storedPath.parent.existsSync()) { + mediaService.storedPath.parent.createSync(recursive: true); + } + mediaService.storedPath.writeAsBytesSync(bytes); + mediaService.thumbnailPath.writeAsBytesSync(bytes); + } + } + + if (mounted) { + showSnackbar( + context, + 'Successfully generated 1000 mock images!', + level: SnackbarLevel.success, + ); + } + } catch (e) { + if (mounted) { + showSnackbar(context, 'Error generating images: $e'); + } + } finally { + if (mounted) { + setState(() { + _isGeneratingMockImages = false; + }); + } + } + } + Future toggleDeveloperSettings() async { await UserService.update((u) => u.isDeveloper = !u.isDeveloper); } @@ -132,6 +350,20 @@ class _DeveloperSettingsViewState extends State { onTap: () => context.push(Routes.settingsDeveloperAutomatedTesting), ), + if (kDebugMode) + ListTile( + title: const Text('Generate 1000 Mock Images'), + trailing: _isGeneratingMockImages + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, + onTap: _isGeneratingMockImages + ? null + : _generate1000MockImages, + ), ListTile( title: const Text('Reopen Setup'), onTap: () async { diff --git a/lib/src/visual/views/settings/subscription/additional_users.view.dart b/lib/src/visual/views/settings/subscription/additional_users.view.dart index 485bb8ea..483cad30 100644 --- a/lib/src/visual/views/settings/subscription/additional_users.view.dart +++ b/lib/src/visual/views/settings/subscription/additional_users.view.dart @@ -57,21 +57,16 @@ class _AdditionalUsersViewState extends State { } Future addAdditionalUser() async { - final selectedUserIds = - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectAdditionalUsers( - limit: _planLimit, - alreadySelected: - ballance?.additionalAccounts - .map((e) => e.userId.toInt()) - .toList() ?? - [], - ), - ), - ) - as List?; + final selectedUserIds = await context.navPush( + SelectAdditionalUsers( + limit: _planLimit, + alreadySelected: + ballance?.additionalAccounts + .map((e) => e.userId.toInt()) + .toList() ?? + [], + ), + ) as List?; if (selectedUserIds == null) return; for (final selectedUserId in selectedUserIds) { final res = await apiService.addAdditionalUser(Int64(selectedUserId)); diff --git a/lib/src/visual/views/settings/subscription/subscription.view.dart b/lib/src/visual/views/settings/subscription/subscription.view.dart index 6d608643..ec0156c7 100644 --- a/lib/src/visual/views/settings/subscription/subscription.view.dart +++ b/lib/src/visual/views/settings/subscription/subscription.view.dart @@ -1,7 +1,5 @@ -// ignore_for_file: inference_failure_on_instance_creation import 'dart:async'; import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; @@ -61,26 +59,69 @@ class _SubscriptionViewState extends State { ), body: ListView( children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Center( - child: Container( - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(15), - ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - child: Text( - currentPlan.name, - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: isDarkMode(context) ? Colors.black : Colors.white, + if (currentPlan.name == SubscriptionPlan.Free.name) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), + child: Column( + children: [ + const SizedBox(height: 20), + Text( + context.lang.subscriptionPledgeTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: context.color.primary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 40), + _MissionRow( + icon: FontAwesomeIcons.shieldHalved, + title: context.lang.subscriptionPledgeSecureTitle, + desc: context.lang.subscriptionPledgeSecureDesc, + ), + const SizedBox(height: 24), + _MissionRow( + icon: FontAwesomeIcons.userSecret, + title: context.lang.subscriptionPledgeNoAdsTitle, + desc: context.lang.subscriptionPledgeNoAdsDesc, + ), + const SizedBox(height: 24), + _MissionRow( + icon: FontAwesomeIcons.heart, + title: context.lang.subscriptionPledgeFundedTitle, + desc: context.lang.subscriptionPledgeFundedDesc, + ), + const SizedBox(height: 24), + ], + ), + ) + else + Padding( + padding: const EdgeInsets.all(32), + child: Center( + child: Container( + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(15), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + child: Text( + currentPlan.name, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), ), ), ), ), - ), + const SizedBox(height: 16), if (additionalOwnerName != null) Center( child: Text( @@ -95,16 +136,6 @@ class _SubscriptionViewState extends State { ), if (!isPayingUser(currentPlan) || currentPlan == SubscriptionPlan.Tester) ...[ - Center( - child: Padding( - padding: const EdgeInsets.all(18), - child: Text( - context.lang.upgradeToPaidPlan, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18), - ), - ), - ), PlanCard( plan: SubscriptionPlan.Pro, onPurchase: initAsync, @@ -152,14 +183,9 @@ class _SubscriptionViewState extends State { text: context.lang.manageAdditionalUsers, subtitle: loaded ? Text('${context.lang.open}: 3') : null, onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return AdditionalUsersView( - ballance: ballance, - ); - }, + await context.navPush( + AdditionalUsersView( + ballance: ballance, ), ); await initAsync(); @@ -347,3 +373,47 @@ class _PlanCardState extends State { ); } } + +class _MissionRow extends StatelessWidget { + const _MissionRow({ + required this.icon, + required this.title, + required this.desc, + }); + + final IconData icon; + final String title; + final String desc; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FaIcon( + icon, + size: 24, + color: context.color.primary.withValues(alpha: 0.8), + ), + const SizedBox(height: 8), + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + desc, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 1.3, + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/shared/memory_item_slider.view.dart b/lib/src/visual/views/shared/memory_item_slider.view.dart deleted file mode 100644 index 752dd775..00000000 --- a/lib/src/visual/views/shared/memory_item_slider.view.dart +++ /dev/null @@ -1,267 +0,0 @@ -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/locator.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/upload.api.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/alert.dialog.dart'; -import 'package:twonly/src/visual/components/snackbar.dart'; -import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; -import 'package:twonly/src/visual/helpers/video_player_file.helper.dart'; -import 'package:twonly/src/visual/views/camera/camera_preview_components/save_to_gallery.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; - -class MemoriesPhotoSliderView extends StatefulWidget { - MemoriesPhotoSliderView({ - required this.galleryItems, - super.key, - this.loadingBuilder, - this.minScale, - this.maxScale, - this.initialIndex = 0, - this.scrollDirection = Axis.horizontal, - }) : pageController = PageController(initialPage: initialIndex); - - final LoadingBuilder? loadingBuilder; - final dynamic minScale; - final dynamic maxScale; - final int initialIndex; - final PageController pageController; - final List galleryItems; - final Axis scrollDirection; - - @override - State createState() { - return _MemoriesPhotoSliderViewState(); - } -} - -class _MemoriesPhotoSliderViewState extends State { - late int currentIndex = widget.initialIndex; - final GlobalKey> key = GlobalKey(); - - void onPageChanged(int index) { - setState(() { - currentIndex = index; - }); - } - - Future deleteFile() async { - final confirmed = await showAlertDialog( - context, - context.lang.deleteImageTitle, - context.lang.deleteImageBody, - ); - - if (!confirmed) return; - - widget.galleryItems[currentIndex].mediaService.fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile( - widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId, - ); - - widget.galleryItems.removeAt(currentIndex); - setState(() {}); - if (mounted) { - Navigator.pop(context, true); - } - } - - Future exportFile() async { - final item = widget.galleryItems[currentIndex].mediaService; - - try { - if (item.mediaFile.type == MediaType.video) { - await saveVideoToGallery(item.storedPath.path); - } else if (item.mediaFile.type == MediaType.image || - item.mediaFile.type == MediaType.gif) { - final imageBytes = await item.storedPath.readAsBytes(); - await saveImageToGallery(imageBytes); - } - if (!mounted) return; - showSnackbar( - context, - context.lang.galleryExportSuccess, - level: SnackbarLevel.success, - ); - } catch (e) { - if (!mounted) return; - showSnackbar( - context, - e.toString(), - level: SnackbarLevel.success, - ); - } - } - - Future shareMediaFile() async { - final orgMediaService = widget.galleryItems[currentIndex].mediaService; - - final newMediaService = await initializeMediaUpload( - orgMediaService.mediaFile.type, - userService.currentUser.defaultShowTime, - ); - if (newMediaService == null) { - Log.error('Could not create new mediaFIle'); - return; - } - - if (orgMediaService.storedPath.existsSync()) { - orgMediaService.storedPath.copySync(newMediaService.originalPath.path); - } else if (orgMediaService.tempPath.existsSync()) { - orgMediaService.tempPath.copySync(newMediaService.originalPath.path); - } - - if (!mounted) return; - - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageEditorView( - mediaFileService: newMediaService, - sharedFromGallery: true, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final orgMediaService = widget.galleryItems[currentIndex].mediaService; - return Dismissible( - key: key, - direction: DismissDirection.vertical, - resizeDuration: null, - onDismissed: (d) { - Navigator.pop(context, false); - }, - child: Scaffold( - backgroundColor: Colors.white.withAlpha(0), - body: Container( - color: context.color.surface, - constraints: BoxConstraints.expand( - height: MediaQuery.of(context).size.height, - ), - child: Stack( - alignment: Alignment.bottomRight, - children: [ - MediaViewSizingHelper( - bottomNavigation: ColoredBox( - color: context.color.surface, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!orgMediaService.storedPath.existsSync()) - Padding( - padding: const EdgeInsets.only(right: 12), - child: SaveToGalleryButton( - isLoading: false, - displayButtonLabel: true, - mediaService: orgMediaService, - ), - ), - FilledButton.icon( - icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: shareMediaFile, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric( - vertical: 10, - horizontal: 30, - ), - ), - backgroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.primary, - ), - ), - label: Text( - context.lang.shareImagedEditorSendImage, - style: const TextStyle(fontSize: 17), - ), - ), - ], - ), - ), - child: Stack( - children: [ - PhotoViewGallery.builder( - scrollPhysics: const BouncingScrollPhysics(), - builder: _buildItem, - itemCount: widget.galleryItems.length, - loadingBuilder: widget.loadingBuilder, - pageController: widget.pageController, - onPageChanged: onPageChanged, - scrollDirection: widget.scrollDirection, - ), - Positioned( - right: 5, - child: PopupMenuButton( - onSelected: (result) async { - if (result == 'delete') { - await deleteFile(); - } - if (result == 'export') { - await exportFile(); - } - }, - itemBuilder: (context) => >[ - PopupMenuItem( - value: 'delete', - child: Text(context.lang.galleryDelete), - ), - PopupMenuItem( - value: 'export', - child: Text(context.lang.galleryExport), - ), - // PopupMenuItem( - // value: 'details', - // child: Text(context.lang.galleryDetails), - // ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { - final item = widget.galleryItems[index]; - - var filePath = item.mediaService.storedPath; - if (!filePath.existsSync()) { - filePath = item.mediaService.tempPath; - } - - return item.mediaService.mediaFile.type == MediaType.video - ? PhotoViewGalleryPageOptions.customChild( - child: VideoPlayerFileHelper( - videoPath: filePath, - ), - initialScale: PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained, - maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes( - tag: item.mediaService.mediaFile.mediaId, - ), - ) - : PhotoViewGalleryPageOptions( - imageProvider: FileImage(filePath), - initialScale: PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained, - maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes( - tag: item.mediaService.mediaFile.mediaId, - ), - ); - } -} diff --git a/lib/src/visual/views/shared/memory_item_thumbnail.comp.dart b/lib/src/visual/views/shared/memory_item_thumbnail.comp.dart deleted file mode 100644 index 9b2638d1..00000000 --- a/lib/src/visual/views/shared/memory_item_thumbnail.comp.dart +++ /dev/null @@ -1,78 +0,0 @@ -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 MemoriesItemThumbnailComp extends StatefulWidget { - const MemoriesItemThumbnailComp({ - required this.galleryItem, - required this.onTap, - super.key, - }); - - final MemoryItem galleryItem; - final GestureTapCallback onTap; - - @override - State createState() => - _MemoriesItemThumbnailCompState(); -} - -class _MemoriesItemThumbnailCompState extends State { - @override - void initState() { - super.initState(); - initAsync(); - } - - Future initAsync() async { - if (!widget.galleryItem.mediaService.thumbnailPath.existsSync()) { - if (widget.galleryItem.mediaService.storedPath.existsSync()) { - await widget.galleryItem.mediaService.createThumbnail(); - if (mounted) setState(() {}); - } - } - } - - @override - void dispose() { - super.dispose(); - } - - String formatDuration(Duration duration) { - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); - final twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); - return '$twoDigitMinutes:$twoDigitSeconds'; - } - - @override - Widget build(BuildContext context) { - final media = widget.galleryItem.mediaService; - return GestureDetector( - onTap: widget.onTap, - child: Hero( - tag: media.mediaFile.mediaId, - child: Stack( - children: [ - if (media.thumbnailPath.existsSync()) - Image.file(media.thumbnailPath) - else if (media.storedPath.existsSync() && - media.mediaFile.type == MediaType.image || - media.mediaFile.type == MediaType.gif) - Image.file(media.storedPath) - else - const Text('Media file removed.'), - if (widget.galleryItem.mediaService.mediaFile.type == - MediaType.video) - const Positioned.fill( - child: Center( - child: FaIcon(FontAwesomeIcons.circlePlay), - ), - ), - ], - ), - ), - ); - } -} diff --git a/test/drift/twonly_db/generated/schema.dart b/test/drift/twonly_db/generated/schema.dart index ba56c197..938ab126 100644 --- a/test/drift/twonly_db/generated/schema.dart +++ b/test/drift/twonly_db/generated/schema.dart @@ -17,6 +17,7 @@ import 'schema_v10.dart' as v10; import 'schema_v11.dart' as v11; import 'schema_v12.dart' as v12; import 'schema_v13.dart' as v13; +import 'schema_v14.dart' as v14; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -48,10 +49,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v12.DatabaseAtV12(db); case 13: return v13.DatabaseAtV13(db); + case 14: + return v14.DatabaseAtV14(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; } diff --git a/test/drift/twonly_db/generated/schema_v14.dart b/test/drift/twonly_db/generated/schema_v14.dart index b71ca169..922d77a7 100644 --- a/test/drift/twonly_db/generated/schema_v14.dart +++ b/test/drift/twonly_db/generated/schema_v14.dart @@ -135,6 +135,27 @@ class Contacts extends Table with TableInfo { requiredDuringInsert: false, $customConstraints: 'NULL', ); + late final GeneratedColumn userDiscoveryExcluded = GeneratedColumn( + 'user_discovery_excluded', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); late final GeneratedColumn mediaSendCounter = GeneratedColumn( 'media_send_counter', aliasedName, @@ -169,6 +190,8 @@ class Contacts extends Table with TableInfo { accountDeleted, createdAt, userDiscoveryVersion, + userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ]; @@ -239,6 +262,14 @@ class Contacts extends Table with TableInfo { DriftSqlType.blob, data['${effectivePrefix}user_discovery_version'], ), + userDiscoveryExcluded: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_excluded'], + )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_manual_approved'], + ), mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -276,6 +307,8 @@ class ContactsData extends DataClass implements Insertable { final int accountDeleted; final int createdAt; final i2.Uint8List? userDiscoveryVersion; + final int userDiscoveryExcluded; + final int? userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const ContactsData({ @@ -293,6 +326,8 @@ class ContactsData extends DataClass implements Insertable { required this.accountDeleted, required this.createdAt, this.userDiscoveryVersion, + required this.userDiscoveryExcluded, + this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -325,6 +360,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion, ); } + map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -354,6 +395,11 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent ? const Value.absent() : Value(userDiscoveryVersion), + userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -385,6 +431,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: serializer.fromJson( json['userDiscoveryVersion'], ), + userDiscoveryExcluded: serializer.fromJson( + json['userDiscoveryExcluded'], + ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), mediaReceivedCounter: serializer.fromJson( json['mediaReceivedCounter'], @@ -413,6 +465,10 @@ class ContactsData extends DataClass implements Insertable { 'userDiscoveryVersion': serializer.toJson( userDiscoveryVersion, ), + 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), }; @@ -433,6 +489,8 @@ class ContactsData extends DataClass implements Insertable { int? accountDeleted, int? createdAt, Value userDiscoveryVersion = const Value.absent(), + int? userDiscoveryExcluded, + Value userDiscoveryManualApproved = const Value.absent(), int? mediaSendCounter, int? mediaReceivedCounter, }) => ContactsData( @@ -454,6 +512,10 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: userDiscoveryVersion.present ? userDiscoveryVersion.value : this.userDiscoveryVersion, + userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -485,6 +547,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: data.userDiscoveryVersion.present ? data.userDiscoveryVersion.value : this.userDiscoveryVersion, + userDiscoveryExcluded: data.userDiscoveryExcluded.present + ? data.userDiscoveryExcluded.value + : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: data.mediaSendCounter.present ? data.mediaSendCounter.value : this.mediaSendCounter, @@ -511,6 +579,8 @@ class ContactsData extends DataClass implements Insertable { ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -533,6 +603,8 @@ class ContactsData extends DataClass implements Insertable { accountDeleted, createdAt, $driftBlobEquality.hash(userDiscoveryVersion), + userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ); @@ -560,6 +632,9 @@ class ContactsData extends DataClass implements Insertable { other.userDiscoveryVersion, this.userDiscoveryVersion, ) && + other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && other.mediaSendCounter == this.mediaSendCounter && other.mediaReceivedCounter == this.mediaReceivedCounter); } @@ -579,6 +654,8 @@ class ContactsCompanion extends UpdateCompanion { final Value accountDeleted; final Value createdAt; final Value userDiscoveryVersion; + final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -596,6 +673,8 @@ class ContactsCompanion extends UpdateCompanion { this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }); @@ -614,6 +693,8 @@ class ContactsCompanion extends UpdateCompanion { this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }) : username = Value(username); @@ -632,6 +713,8 @@ class ContactsCompanion extends UpdateCompanion { Expression? accountDeleted, Expression? createdAt, Expression? userDiscoveryVersion, + Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, Expression? mediaSendCounter, Expression? mediaReceivedCounter, }) { @@ -653,6 +736,10 @@ class ContactsCompanion extends UpdateCompanion { if (createdAt != null) 'created_at': createdAt, if (userDiscoveryVersion != null) 'user_discovery_version': userDiscoveryVersion, + if (userDiscoveryExcluded != null) + 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaReceivedCounter != null) 'media_received_counter': mediaReceivedCounter, @@ -674,6 +761,8 @@ class ContactsCompanion extends UpdateCompanion { Value? accountDeleted, Value? createdAt, Value? userDiscoveryVersion, + Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -692,6 +781,10 @@ class ContactsCompanion extends UpdateCompanion { accountDeleted: accountDeleted ?? this.accountDeleted, createdAt: createdAt ?? this.createdAt, userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, + userDiscoveryExcluded: + userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -746,6 +839,16 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryVersion.value, ); } + if (userDiscoveryExcluded.present) { + map['user_discovery_excluded'] = Variable( + userDiscoveryExcluded.value, + ); + } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } if (mediaSendCounter.present) { map['media_send_counter'] = Variable(mediaSendCounter.value); } @@ -772,6 +875,8 @@ class ContactsCompanion extends UpdateCompanion { ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -2031,6 +2136,25 @@ class MediaFiles extends Table with TableInfo { $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft_media IN (0, 1))', defaultValue: const CustomExpression('0'), ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasCropAnalyzed = GeneratedColumn( + 'has_crop_analyzed', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); late final GeneratedColumn preProgressingProcess = GeneratedColumn( 'pre_progressing_process', aliasedName, @@ -2122,6 +2246,14 @@ class MediaFiles extends Table with TableInfo { 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', ), ); + late final GeneratedColumn createdAtMonth = GeneratedColumn( + 'created_at_month', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); @override List get $columns => [ mediaId, @@ -2131,6 +2263,8 @@ class MediaFiles extends Table with TableInfo { requiresAuthentication, stored, isDraftMedia, + isFavorite, + hasCropAnalyzed, preProgressingProcess, reuploadRequestedBy, displayLimitInMilliseconds, @@ -2141,6 +2275,7 @@ class MediaFiles extends Table with TableInfo { encryptionNonce, storedFileHash, createdAt, + createdAtMonth, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2181,6 +2316,14 @@ class MediaFiles extends Table with TableInfo { DriftSqlType.int, data['${effectivePrefix}is_draft_media'], )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + hasCropAnalyzed: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_crop_analyzed'], + )!, preProgressingProcess: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}pre_progressing_process'], @@ -2221,6 +2364,10 @@ class MediaFiles extends Table with TableInfo { DriftSqlType.int, data['${effectivePrefix}created_at'], )!, + createdAtMonth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_month'], + ), ); } @@ -2243,6 +2390,8 @@ class MediaFilesData extends DataClass implements Insertable { final int requiresAuthentication; final int stored; final int isDraftMedia; + final int isFavorite; + final int hasCropAnalyzed; final int? preProgressingProcess; final String? reuploadRequestedBy; final int? displayLimitInMilliseconds; @@ -2253,6 +2402,7 @@ class MediaFilesData extends DataClass implements Insertable { final i2.Uint8List? encryptionNonce; final i2.Uint8List? storedFileHash; final int createdAt; + final String? createdAtMonth; const MediaFilesData({ required this.mediaId, required this.type, @@ -2261,6 +2411,8 @@ class MediaFilesData extends DataClass implements Insertable { required this.requiresAuthentication, required this.stored, required this.isDraftMedia, + required this.isFavorite, + required this.hasCropAnalyzed, this.preProgressingProcess, this.reuploadRequestedBy, this.displayLimitInMilliseconds, @@ -2271,6 +2423,7 @@ class MediaFilesData extends DataClass implements Insertable { this.encryptionNonce, this.storedFileHash, required this.createdAt, + this.createdAtMonth, }); @override Map toColumns(bool nullToAbsent) { @@ -2286,6 +2439,8 @@ class MediaFilesData extends DataClass implements Insertable { map['requires_authentication'] = Variable(requiresAuthentication); map['stored'] = Variable(stored); map['is_draft_media'] = Variable(isDraftMedia); + map['is_favorite'] = Variable(isFavorite); + map['has_crop_analyzed'] = Variable(hasCropAnalyzed); if (!nullToAbsent || preProgressingProcess != null) { map['pre_progressing_process'] = Variable(preProgressingProcess); } @@ -2316,6 +2471,9 @@ class MediaFilesData extends DataClass implements Insertable { map['stored_file_hash'] = Variable(storedFileHash); } map['created_at'] = Variable(createdAt); + if (!nullToAbsent || createdAtMonth != null) { + map['created_at_month'] = Variable(createdAtMonth); + } return map; } @@ -2332,6 +2490,8 @@ class MediaFilesData extends DataClass implements Insertable { requiresAuthentication: Value(requiresAuthentication), stored: Value(stored), isDraftMedia: Value(isDraftMedia), + isFavorite: Value(isFavorite), + hasCropAnalyzed: Value(hasCropAnalyzed), preProgressingProcess: preProgressingProcess == null && nullToAbsent ? const Value.absent() : Value(preProgressingProcess), @@ -2361,6 +2521,9 @@ class MediaFilesData extends DataClass implements Insertable { ? const Value.absent() : Value(storedFileHash), createdAt: Value(createdAt), + createdAtMonth: createdAtMonth == null && nullToAbsent + ? const Value.absent() + : Value(createdAtMonth), ); } @@ -2379,6 +2542,8 @@ class MediaFilesData extends DataClass implements Insertable { ), stored: serializer.fromJson(json['stored']), isDraftMedia: serializer.fromJson(json['isDraftMedia']), + isFavorite: serializer.fromJson(json['isFavorite']), + hasCropAnalyzed: serializer.fromJson(json['hasCropAnalyzed']), preProgressingProcess: serializer.fromJson( json['preProgressingProcess'], ), @@ -2399,6 +2564,7 @@ class MediaFilesData extends DataClass implements Insertable { json['storedFileHash'], ), createdAt: serializer.fromJson(json['createdAt']), + createdAtMonth: serializer.fromJson(json['createdAtMonth']), ); } @override @@ -2412,6 +2578,8 @@ class MediaFilesData extends DataClass implements Insertable { 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'stored': serializer.toJson(stored), 'isDraftMedia': serializer.toJson(isDraftMedia), + 'isFavorite': serializer.toJson(isFavorite), + 'hasCropAnalyzed': serializer.toJson(hasCropAnalyzed), 'preProgressingProcess': serializer.toJson(preProgressingProcess), 'reuploadRequestedBy': serializer.toJson(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson( @@ -2424,6 +2592,7 @@ class MediaFilesData extends DataClass implements Insertable { 'encryptionNonce': serializer.toJson(encryptionNonce), 'storedFileHash': serializer.toJson(storedFileHash), 'createdAt': serializer.toJson(createdAt), + 'createdAtMonth': serializer.toJson(createdAtMonth), }; } @@ -2435,6 +2604,8 @@ class MediaFilesData extends DataClass implements Insertable { int? requiresAuthentication, int? stored, int? isDraftMedia, + int? isFavorite, + int? hasCropAnalyzed, Value preProgressingProcess = const Value.absent(), Value reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), @@ -2445,6 +2616,7 @@ class MediaFilesData extends DataClass implements Insertable { Value encryptionNonce = const Value.absent(), Value storedFileHash = const Value.absent(), int? createdAt, + Value createdAtMonth = const Value.absent(), }) => MediaFilesData( mediaId: mediaId ?? this.mediaId, type: type ?? this.type, @@ -2456,6 +2628,8 @@ class MediaFilesData extends DataClass implements Insertable { requiresAuthentication ?? this.requiresAuthentication, stored: stored ?? this.stored, isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, preProgressingProcess: preProgressingProcess.present ? preProgressingProcess.value : this.preProgressingProcess, @@ -2482,6 +2656,9 @@ class MediaFilesData extends DataClass implements Insertable { ? storedFileHash.value : this.storedFileHash, createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth.present + ? createdAtMonth.value + : this.createdAtMonth, ); MediaFilesData copyWithCompanion(MediaFilesCompanion data) { return MediaFilesData( @@ -2500,6 +2677,12 @@ class MediaFilesData extends DataClass implements Insertable { isDraftMedia: data.isDraftMedia.present ? data.isDraftMedia.value : this.isDraftMedia, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + hasCropAnalyzed: data.hasCropAnalyzed.present + ? data.hasCropAnalyzed.value + : this.hasCropAnalyzed, preProgressingProcess: data.preProgressingProcess.present ? data.preProgressingProcess.value : this.preProgressingProcess, @@ -2528,6 +2711,9 @@ class MediaFilesData extends DataClass implements Insertable { ? data.storedFileHash.value : this.storedFileHash, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + createdAtMonth: data.createdAtMonth.present + ? data.createdAtMonth.value + : this.createdAtMonth, ); } @@ -2541,6 +2727,8 @@ class MediaFilesData extends DataClass implements Insertable { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') ..write('preProgressingProcess: $preProgressingProcess, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') @@ -2550,7 +2738,8 @@ class MediaFilesData extends DataClass implements Insertable { ..write('encryptionMac: $encryptionMac, ') ..write('encryptionNonce: $encryptionNonce, ') ..write('storedFileHash: $storedFileHash, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth') ..write(')')) .toString(); } @@ -2564,6 +2753,8 @@ class MediaFilesData extends DataClass implements Insertable { requiresAuthentication, stored, isDraftMedia, + isFavorite, + hasCropAnalyzed, preProgressingProcess, reuploadRequestedBy, displayLimitInMilliseconds, @@ -2574,6 +2765,7 @@ class MediaFilesData extends DataClass implements Insertable { $driftBlobEquality.hash(encryptionNonce), $driftBlobEquality.hash(storedFileHash), createdAt, + createdAtMonth, ); @override bool operator ==(Object other) => @@ -2586,6 +2778,8 @@ class MediaFilesData extends DataClass implements Insertable { other.requiresAuthentication == this.requiresAuthentication && other.stored == this.stored && other.isDraftMedia == this.isDraftMedia && + other.isFavorite == this.isFavorite && + other.hasCropAnalyzed == this.hasCropAnalyzed && other.preProgressingProcess == this.preProgressingProcess && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && @@ -2601,7 +2795,8 @@ class MediaFilesData extends DataClass implements Insertable { other.storedFileHash, this.storedFileHash, ) && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.createdAtMonth == this.createdAtMonth); } class MediaFilesCompanion extends UpdateCompanion { @@ -2612,6 +2807,8 @@ class MediaFilesCompanion extends UpdateCompanion { final Value requiresAuthentication; final Value stored; final Value isDraftMedia; + final Value isFavorite; + final Value hasCropAnalyzed; final Value preProgressingProcess; final Value reuploadRequestedBy; final Value displayLimitInMilliseconds; @@ -2622,6 +2819,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value encryptionNonce; final Value storedFileHash; final Value createdAt; + final Value createdAtMonth; final Value rowid; const MediaFilesCompanion({ this.mediaId = const Value.absent(), @@ -2631,6 +2829,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), this.preProgressingProcess = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), @@ -2641,6 +2841,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.storedFileHash = const Value.absent(), this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), }); MediaFilesCompanion.insert({ @@ -2651,6 +2852,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), this.preProgressingProcess = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), @@ -2661,6 +2864,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.storedFileHash = const Value.absent(), this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), }) : mediaId = Value(mediaId), type = Value(type); @@ -2672,6 +2876,8 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? requiresAuthentication, Expression? stored, Expression? isDraftMedia, + Expression? isFavorite, + Expression? hasCropAnalyzed, Expression? preProgressingProcess, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, @@ -2682,6 +2888,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? encryptionNonce, Expression? storedFileHash, Expression? createdAt, + Expression? createdAtMonth, Expression? rowid, }) { return RawValuesInsertable({ @@ -2693,6 +2900,8 @@ class MediaFilesCompanion extends UpdateCompanion { 'requires_authentication': requiresAuthentication, if (stored != null) 'stored': stored, if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (isFavorite != null) 'is_favorite': isFavorite, + if (hasCropAnalyzed != null) 'has_crop_analyzed': hasCropAnalyzed, if (preProgressingProcess != null) 'pre_progressing_process': preProgressingProcess, if (reuploadRequestedBy != null) @@ -2706,6 +2915,7 @@ class MediaFilesCompanion extends UpdateCompanion { if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, if (storedFileHash != null) 'stored_file_hash': storedFileHash, if (createdAt != null) 'created_at': createdAt, + if (createdAtMonth != null) 'created_at_month': createdAtMonth, if (rowid != null) 'rowid': rowid, }); } @@ -2718,6 +2928,8 @@ class MediaFilesCompanion extends UpdateCompanion { Value? requiresAuthentication, Value? stored, Value? isDraftMedia, + Value? isFavorite, + Value? hasCropAnalyzed, Value? preProgressingProcess, Value? reuploadRequestedBy, Value? displayLimitInMilliseconds, @@ -2728,6 +2940,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? encryptionNonce, Value? storedFileHash, Value? createdAt, + Value? createdAtMonth, Value? rowid, }) { return MediaFilesCompanion( @@ -2739,6 +2952,8 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication ?? this.requiresAuthentication, stored: stored ?? this.stored, isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, preProgressingProcess: preProgressingProcess ?? this.preProgressingProcess, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, @@ -2751,6 +2966,7 @@ class MediaFilesCompanion extends UpdateCompanion { encryptionNonce: encryptionNonce ?? this.encryptionNonce, storedFileHash: storedFileHash ?? this.storedFileHash, createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth ?? this.createdAtMonth, rowid: rowid ?? this.rowid, ); } @@ -2781,6 +2997,12 @@ class MediaFilesCompanion extends UpdateCompanion { if (isDraftMedia.present) { map['is_draft_media'] = Variable(isDraftMedia.value); } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (hasCropAnalyzed.present) { + map['has_crop_analyzed'] = Variable(hasCropAnalyzed.value); + } if (preProgressingProcess.present) { map['pre_progressing_process'] = Variable( preProgressingProcess.value, @@ -2817,6 +3039,9 @@ class MediaFilesCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (createdAtMonth.present) { + map['created_at_month'] = Variable(createdAtMonth.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2833,6 +3058,8 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') ..write('preProgressingProcess: $preProgressingProcess, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') @@ -2843,6 +3070,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('encryptionNonce: $encryptionNonce, ') ..write('storedFileHash: $storedFileHash, ') ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -7416,12 +7644,21 @@ class KeyVerifications extends Table final GeneratedDatabase attachedDatabase; final String? _alias; KeyVerifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); late final GeneratedColumn contactId = GeneratedColumn( 'contact_id', aliasedName, false, type: DriftSqlType.int, - requiredDuringInsert: false, + requiredDuringInsert: true, $customConstraints: 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', ); @@ -7446,18 +7683,27 @@ class KeyVerifications extends Table ), ); @override - List get $columns => [contactId, type, createdAt]; + List get $columns => [ + verificationId, + contactId, + type, + createdAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'key_verifications'; @override - Set get $primaryKey => {contactId}; + Set get $primaryKey => {verificationId}; @override KeyVerificationsData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return KeyVerificationsData( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, contactId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}contact_id'], @@ -7478,18 +7724,18 @@ class KeyVerifications extends Table return KeyVerifications(attachedDatabase, alias); } - @override - List get customConstraints => const ['PRIMARY KEY(contact_id)']; @override bool get dontWriteConstraints => true; } class KeyVerificationsData extends DataClass implements Insertable { + final int verificationId; final int contactId; final String type; final int createdAt; const KeyVerificationsData({ + required this.verificationId, required this.contactId, required this.type, required this.createdAt, @@ -7497,6 +7743,7 @@ class KeyVerificationsData extends DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; + map['verification_id'] = Variable(verificationId); map['contact_id'] = Variable(contactId); map['type'] = Variable(type); map['created_at'] = Variable(createdAt); @@ -7505,6 +7752,7 @@ class KeyVerificationsData extends DataClass KeyVerificationsCompanion toCompanion(bool nullToAbsent) { return KeyVerificationsCompanion( + verificationId: Value(verificationId), contactId: Value(contactId), type: Value(type), createdAt: Value(createdAt), @@ -7517,6 +7765,7 @@ class KeyVerificationsData extends DataClass }) { serializer ??= driftRuntimeOptions.defaultSerializer; return KeyVerificationsData( + verificationId: serializer.fromJson(json['verificationId']), contactId: serializer.fromJson(json['contactId']), type: serializer.fromJson(json['type']), createdAt: serializer.fromJson(json['createdAt']), @@ -7526,6 +7775,7 @@ class KeyVerificationsData extends DataClass Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'verificationId': serializer.toJson(verificationId), 'contactId': serializer.toJson(contactId), 'type': serializer.toJson(type), 'createdAt': serializer.toJson(createdAt), @@ -7533,16 +7783,21 @@ class KeyVerificationsData extends DataClass } KeyVerificationsData copyWith({ + int? verificationId, int? contactId, String? type, int? createdAt, }) => KeyVerificationsData( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, ); KeyVerificationsData copyWithCompanion(KeyVerificationsCompanion data) { return KeyVerificationsData( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, contactId: data.contactId.present ? data.contactId.value : this.contactId, type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -7552,6 +7807,7 @@ class KeyVerificationsData extends DataClass @override String toString() { return (StringBuffer('KeyVerificationsData(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -7560,36 +7816,43 @@ class KeyVerificationsData extends DataClass } @override - int get hashCode => Object.hash(contactId, type, createdAt); + int get hashCode => Object.hash(verificationId, contactId, type, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is KeyVerificationsData && + other.verificationId == this.verificationId && other.contactId == this.contactId && other.type == this.type && other.createdAt == this.createdAt); } class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; final Value contactId; final Value type; final Value createdAt; const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), this.contactId = const Value.absent(), this.type = const Value.absent(), this.createdAt = const Value.absent(), }); KeyVerificationsCompanion.insert({ - this.contactId = const Value.absent(), + this.verificationId = const Value.absent(), + required int contactId, required String type, this.createdAt = const Value.absent(), - }) : type = Value(type); + }) : contactId = Value(contactId), + type = Value(type); static Insertable custom({ + Expression? verificationId, Expression? contactId, Expression? type, Expression? createdAt, }) { return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, if (contactId != null) 'contact_id': contactId, if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, @@ -7597,11 +7860,13 @@ class KeyVerificationsCompanion extends UpdateCompanion { } KeyVerificationsCompanion copyWith({ + Value? verificationId, Value? contactId, Value? type, Value? createdAt, }) { return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, @@ -7611,6 +7876,9 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } if (contactId.present) { map['contact_id'] = Variable(contactId.value); } @@ -7626,6 +7894,7 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override String toString() { return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -8575,7 +8844,7 @@ class UserDiscoveryOtherPromotions extends Table String get actualTableName => $name; static const String $name = 'user_discovery_other_promotions'; @override - Set get $primaryKey => {fromContactId, promotionId}; + Set get $primaryKey => {fromContactId, publicId}; @override UserDiscoveryOtherPromotionsData map( Map data, { @@ -8617,7 +8886,7 @@ class UserDiscoveryOtherPromotions extends Table @override List get customConstraints => const [ - 'PRIMARY KEY(from_contact_id, promotion_id)', + 'PRIMARY KEY(from_contact_id, public_id)', ]; @override bool get dontWriteConstraints => true; @@ -9352,6 +9621,419 @@ class UserDiscoverySharesCompanion } } +class Shortcuts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Shortcuts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn usageCounter = GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [id, emoji, usageCounter]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcuts'; + @override + Set get $primaryKey => {id}; + @override + ShortcutsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + usageCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_counter'], + )!, + ); + } + + @override + Shortcuts createAlias(String alias) { + return Shortcuts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ShortcutsData extends DataClass implements Insertable { + final int id; + final String emoji; + final int usageCounter; + const ShortcutsData({ + required this.id, + required this.emoji, + required this.usageCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['emoji'] = Variable(emoji); + map['usage_counter'] = Variable(usageCounter); + return map; + } + + ShortcutsCompanion toCompanion(bool nullToAbsent) { + return ShortcutsCompanion( + id: Value(id), + emoji: Value(emoji), + usageCounter: Value(usageCounter), + ); + } + + factory ShortcutsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutsData( + id: serializer.fromJson(json['id']), + emoji: serializer.fromJson(json['emoji']), + usageCounter: serializer.fromJson(json['usageCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'emoji': serializer.toJson(emoji), + 'usageCounter': serializer.toJson(usageCounter), + }; + } + + ShortcutsData copyWith({int? id, String? emoji, int? usageCounter}) => + ShortcutsData( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + ShortcutsData copyWithCompanion(ShortcutsCompanion data) { + return ShortcutsData( + id: data.id.present ? data.id.value : this.id, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + usageCounter: data.usageCounter.present + ? data.usageCounter.value + : this.usageCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutsData(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, emoji, usageCounter); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutsData && + other.id == this.id && + other.emoji == this.emoji && + other.usageCounter == this.usageCounter); +} + +class ShortcutsCompanion extends UpdateCompanion { + final Value id; + final Value emoji; + final Value usageCounter; + const ShortcutsCompanion({ + this.id = const Value.absent(), + this.emoji = const Value.absent(), + this.usageCounter = const Value.absent(), + }); + ShortcutsCompanion.insert({ + this.id = const Value.absent(), + required String emoji, + this.usageCounter = const Value.absent(), + }) : emoji = Value(emoji); + static Insertable custom({ + Expression? id, + Expression? emoji, + Expression? usageCounter, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (emoji != null) 'emoji': emoji, + if (usageCounter != null) 'usage_counter': usageCounter, + }); + } + + ShortcutsCompanion copyWith({ + Value? id, + Value? emoji, + Value? usageCounter, + }) { + return ShortcutsCompanion( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (usageCounter.present) { + map['usage_counter'] = Variable(usageCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutsCompanion(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } +} + +class ShortcutMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ShortcutMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shortcutId = GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shortcutId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcut_members'; + @override + Set get $primaryKey => {shortcutId, groupId}; + @override + ShortcutMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutMembersData( + shortcutId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}shortcut_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + ShortcutMembers createAlias(String alias) { + return ShortcutMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(shortcut_id, group_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ShortcutMembersData extends DataClass + implements Insertable { + final int shortcutId; + final String groupId; + const ShortcutMembersData({required this.shortcutId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shortcut_id'] = Variable(shortcutId); + map['group_id'] = Variable(groupId); + return map; + } + + ShortcutMembersCompanion toCompanion(bool nullToAbsent) { + return ShortcutMembersCompanion( + shortcutId: Value(shortcutId), + groupId: Value(groupId), + ); + } + + factory ShortcutMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutMembersData( + shortcutId: serializer.fromJson(json['shortcutId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shortcutId': serializer.toJson(shortcutId), + 'groupId': serializer.toJson(groupId), + }; + } + + ShortcutMembersData copyWith({int? shortcutId, String? groupId}) => + ShortcutMembersData( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + ); + ShortcutMembersData copyWithCompanion(ShortcutMembersCompanion data) { + return ShortcutMembersData( + shortcutId: data.shortcutId.present + ? data.shortcutId.value + : this.shortcutId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersData(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(shortcutId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutMembersData && + other.shortcutId == this.shortcutId && + other.groupId == this.groupId); +} + +class ShortcutMembersCompanion extends UpdateCompanion { + final Value shortcutId; + final Value groupId; + final Value rowid; + const ShortcutMembersCompanion({ + this.shortcutId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShortcutMembersCompanion.insert({ + required int shortcutId, + required String groupId, + this.rowid = const Value.absent(), + }) : shortcutId = Value(shortcutId), + groupId = Value(groupId); + static Insertable custom({ + Expression? shortcutId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shortcutId != null) 'shortcut_id': shortcutId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + ShortcutMembersCompanion copyWith({ + Value? shortcutId, + Value? groupId, + Value? rowid, + }) { + return ShortcutMembersCompanion( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shortcutId.present) { + map['shortcut_id'] = Variable(shortcutId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersCompanion(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class DatabaseAtV14 extends GeneratedDatabase { DatabaseAtV14(QueryExecutor e) : super(e); late final Contacts contacts = Contacts(this); @@ -9386,6 +10068,8 @@ class DatabaseAtV14 extends GeneratedDatabase { late final UserDiscoveryShares userDiscoveryShares = UserDiscoveryShares( this, ); + late final Shortcuts shortcuts = Shortcuts(this); + late final ShortcutMembers shortcutMembers = ShortcutMembers(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -9413,6 +10097,8 @@ class DatabaseAtV14 extends GeneratedDatabase { userDiscoveryOtherPromotions, userDiscoveryOwnPromotions, userDiscoveryShares, + shortcuts, + shortcutMembers, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ @@ -9550,6 +10236,20 @@ class DatabaseAtV14 extends GeneratedDatabase { ), result: [TableUpdate('user_discovery_shares', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'shortcuts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), ]); @override int get schemaVersion => 14;